def test_sync_pull_deleted_obs(self): """ We have an observation in our database with a iNaturalist ID, but when we pull from the iNaturalist API this observation is not returned. So we check the observation individually and in this case we conclude it no longer exist. In this scenario, the observation should be deleted locally """ # Create an Individual that already exists in iNaturalist after a previous push ind = Individual(inaturalist_id=30, latitude=51.2003, longitude=4.9067, observation_time=datetime(2019, 4, 1, 10), originates_in_vespawatch=True, taxon=self.vv_taxon) ind.save() # Set a return value for the self.get_all_mock. It should return no observations self.get_all_mock.return_value = [] # Set a side effect for the self.get_obs_mock. It should raise a ObservationNotFound exception self.get_obs_mock.side_effect = ObservationNotFound() # Run inaturalist sync. call_command('inaturalist_sync') # Assert that the individual is deleted self.assertEqual(len(Individual.objects.all()), 0)
def test_sync_pull_obs_taxon_changed_unknown(self): """ We have an observation in our database with a iNaturalist ID, but when we pull from the iNaturalist API this observation is not returned. So we check the observation individually and in this case we conclude the taxon is not known in our database It doesn't matter where this observation comes from, we should flag it as "unknown taxon" """ # Create an Individual that already exists in iNaturalist after a previous push ind = Individual(inaturalist_id=30, latitude=51.2003, longitude=4.9067, observation_time=datetime(2019, 4, 1, 10), originates_in_vespawatch=True, taxon=self.vv_taxon) ind.save() # Set a return value for the self.get_all_mock. It should return no observations self.get_all_mock.return_value = [] # Set a side effect for the self.get_obs_mock. It should return an observation that has no vespawatch project id self.get_obs_mock.return_value = { 'id': 30, 'taxon': { 'id': 2732, 'name': 'Unknown taxon' }, 'description': '', 'geojson': { 'coordinates': [10, 20] }, 'observed_on_string': '2019-04-01T11:20:00+00:00', 'observed_time_zone': 'Europe/Brussels', 'photos': [], 'project_ids': [11] # some random project } # Assert that the individual had no warning before the sync self.assertEqual( len( Individual.objects.filter( inaturalist_id=30)[0].warnings.all()), 0) # Run inaturalist sync. call_command('inaturalist_sync') # Assert that the individual has a warning now self.assertEqual( len( Individual.objects.filter( inaturalist_id=30)[0].warnings.all()), 2) warning_texts = sorted([ x.text for x in Individual.objects.filter( inaturalist_id=30)[0].warnings.all() ]) self.assertEqual(warning_texts[0], 'not in vespawatch project') self.assertEqual(warning_texts[1], 'unknown taxon') # Assert that the inaturalist_species field was set to the taxon name self.assertEqual( Individual.objects.filter( inaturalist_id=30)[0].inaturalist_species, 'Unknown taxon') self.assertEqual( Individual.objects.filter(inaturalist_id=30)[0].taxon, None)
def test_sync_pull_obs_taxon_changed_known(self): """ We have an observation in our database with a iNaturalist ID, but when we pull from the iNaturalist API this observation is not returned. So we check the observation individually and in this case we conclude the taxon is not Vespa velutina (so the project id is not the one we expect) but it is another taxon that is known in our database. In this case, we should flag it as `not in vespawatch project` but we should not flag it as `taxon unkown` """ # Create an Individual that already exists in iNaturalist after a previous push ind = Individual(inaturalist_id=30, latitude=51.2003, longitude=4.9067, observation_time=datetime(2019, 4, 1, 10), originates_in_vespawatch=True, taxon=self.vv_taxon) ind.save() # Set a return value for the self.get_all_mock. It should return no observations self.get_all_mock.return_value = [] # Set a side effect for the self.get_obs_mock. It should return an observation that has no vespawatch project id self.get_obs_mock.return_value = { 'id': 30, 'taxon': { 'id': self.other_taxon.inaturalist_pull_taxon_ids[0] }, 'description': '', 'geojson': { 'coordinates': [10, 20] }, 'observed_on_string': '2019-04-01T11:20:00+00:00', 'observed_time_zone': 'Europe/Brussels', 'photos': [], 'project_ids': [11] # some random project } # Assert that the individual had no warning before the sync self.assertEqual( len( Individual.objects.filter( inaturalist_id=30)[0].warnings.all()), 0) # Run inaturalist sync. call_command('inaturalist_sync') # Assert that the individual has a warning now self.assertEqual( len( Individual.objects.filter( inaturalist_id=30)[0].warnings.all()), 1) self.assertEqual( Individual.objects.filter( inaturalist_id=30)[0].warnings.all()[0].text, 'not in vespawatch project') # Make sure the taxon is also updated self.assertEqual( Individual.objects.filter(inaturalist_id=30)[0].taxon.id, self.other_taxon.id)
def test_sync_pull_obs_out_project(self): """ We have an observation in our database with a iNaturalist ID, but when we pull from the iNaturalist API this observation is not returned. So we check the observation individually and in this case we conclude it is no longer part of the Vespawatch project. It doesn't matter where this observation comes from, we should flag it as "not in vespawatch project" """ # Create an Individual that already exists in iNaturalist after a previous push ind = Individual(inaturalist_id=30, latitude=51.2003, longitude=4.9067, observation_time=datetime(2019, 4, 1, 10), originates_in_vespawatch=True, taxon=self.vv_taxon) ind.save() # Set a return value for the self.get_all_mock. It should return no observations self.get_all_mock.return_value = [] # Set a side effect for the self.get_obs_mock. It should return an observation that has no vespawatch project id self.get_obs_mock.return_value = { 'id': 30, 'taxon': { 'id': self.vv_taxon.inaturalist_pull_taxon_ids[0] }, 'description': '', 'geojson': { 'coordinates': [10, 20] }, 'observed_on_string': '2019-04-01T11:20:00+00:00', 'observed_time_zone': 'Europe/Brussels', 'photos': [], 'project_ids': [999] # not vespawatch } # Assert that the individual had no warning before the sync self.assertEqual( len( Individual.objects.filter( inaturalist_id=30)[0].warnings.all()), 0) self.assertEqual(len(Taxon.objects.all()), 2) # Run inaturalist sync. call_command('inaturalist_sync') # Assert that the individual has a warning now self.assertEqual( len( Individual.objects.filter( inaturalist_id=30)[0].warnings.all()), 1) self.assertEqual( Individual.objects.filter( inaturalist_id=30)[0].warnings.all()[0].text, 'not in vespawatch project')
def test_sync_pull_obs_vw_evidence_changed_to_nest_orig_inat(self): """ An individual was flagged as a nest on iNaturalist -> the observation originated in inaturalist => delete the individual and create a nest """ # Create an Individual that already exists in iNaturalist after a previous push ind = Individual(inaturalist_id=30, latitude=51.2003, longitude=4.9067, observation_time=datetime(2019, 4, 1, 10), originates_in_vespawatch=False, taxon=self.vv_taxon) ind.save() self.get_all_mock.return_value = [{ 'id': 30, 'description': '', 'geojson': { 'coordinates': [10, 20] }, 'ofvs': [{ 'field_id': settings.VESPAWATCH_EVIDENCE_OBS_FIELD_ID, 'value': 'nest' }], 'observed_on_string': '2019-04-01T11:20:00+00:00', 'observed_time_zone': 'Europe/Brussels', 'photos': [], 'taxon': { 'id': self.vv_taxon.inaturalist_pull_taxon_ids[0] } }] # Assert no nests exist before syncing self.assertEqual(len(Nest.objects.all()), 0) # Run inaturalist sync. call_command('inaturalist_sync') # Assert that the nest exists now self.assertEqual(len(Nest.objects.all()), 1) # Assert that the individual is deleted self.assertEqual(len(Individual.objects.all()), 0) # And that it is not added to the InatObsToDelete (otherwise we will delete it on iNaturalist at the next push) self.assertEqual(len(InatObsToDelete.objects.all()), 0)
def test_sync_pull_update_fields_ind(self): """ When an individual exists and we get updated info from iNaturalist for that individual, the fields should be updated """ # Create an Individual that already exists in iNaturalist after a previous push ind = Individual(inaturalist_id=30, latitude=51.2003, longitude=4.9067, observation_time=datetime(2019, 4, 1, 10), originates_in_vespawatch=True, taxon=self.vv_taxon) ind.save() # Set a return value for the self.get_all_mock. It should return only data for the observation # with iNaturalist id = 30 # Here we will assume the community_taxon_id is set to a vespawatch taxon and the coordiantes are changed self.get_all_mock.return_value = [{ 'id': 30, 'community_taxon_id': self.vv_taxon.inaturalist_pull_taxon_ids[0], 'description': 'test description', 'geojson': { 'coordinates': [10, 20] }, 'observed_on_string': '2019-04-01T11:20:00+00:00', 'observed_time_zone': 'Europe/Brussels', 'taxon': { 'id': self.vv_taxon.inaturalist_pull_taxon_ids[0] }, 'photos': [] }] # Run inaturalist sync. call_command('inaturalist_sync') self.get_all_mock.assert_called_once() # The individual is now changed ind = Individual.objects.all().filter(inaturalist_id=30)[0] self.assertEqual(ind.longitude, 10) self.assertEqual(ind.latitude, 20) self.assertTrue(ind.inat_vv_confirmed) self.assertEqual(ind.comments, 'test description')
def test_sync_push_new_individual(self): """ Create a test individual and run the sync command. Assert that the mocked iNaturalist API is called with the individual data + the vespawatch_evidence field set to 'individual' """ # Create an Individual with minimum info ind = Individual(originates_in_vespawatch=True, taxon=self.vv_taxon, observation_time=datetime(2019, 4, 1, 10), latitude=51.2003, longitude=4.9067) ind.save() individual_data_to_inaturalist = { 'taxon_id': ind.taxon.inaturalist_push_taxon_id, 'observed_on_string': '2019-04-01T08:00:00+00:00', 'time_zone': 'Brussels', 'description': '', 'latitude': ind.latitude, 'longitude': ind.longitude, 'place_guess': '', 'observation_field_values_attributes': [{ 'observation_field_id': settings.VESPAWATCH_ID_OBS_FIELD_ID, 'value': ind.pk }, { 'observation_field_id': settings.VESPAWATCH_EVIDENCE_OBS_FIELD_ID, 'value': 'individual' }] } call_command('inaturalist_sync', pushonly=True) self.create_at_inat_mock.assert_called_once() self.create_at_inat_mock.assert_called_with( access_token='TESTTOKEN', params={'observation': individual_data_to_inaturalist})
def test_sync_pull_obs_vw_evidence_changed_to_nest_orig_vw(self): """ An individual was flagged as a nest on iNaturalist -> the observation originated in vespawatch => flag as "nest at inaturalist" """ # Create an Individual that already exists in iNaturalist after a previous push ind = Individual(inaturalist_id=30, latitude=51.2003, longitude=4.9067, observation_time=datetime(2019, 4, 1, 10), originates_in_vespawatch=True, taxon=self.vv_taxon) ind.save() self.get_all_mock.return_value = [{ 'id': 30, 'description': '', 'geojson': { 'coordinates': [10, 20] }, 'observed_on_string': '2019-04-01T11:20:00+00:00', 'observed_time_zone': 'Europe/Brussels', 'ofvs': [{ 'field_id': settings.VESPAWATCH_EVIDENCE_OBS_FIELD_ID, 'value': 'nest' }], 'taxon': { 'id': self.vv_taxon.inaturalist_pull_taxon_ids[0] }, 'photos': [] }] # Run inaturalist sync. call_command('inaturalist_sync') # Assert that the nest has a warning now self.assertEqual(len(ind.warnings.all()), 1) self.assertEqual(ind.warnings.all()[0].text, 'nest at inaturalist')
def test_sync_no_push_deleted_nest(self): """ An observation that was deleted on vespawatch before it was synced to iNaturalist should not be deleted on iNaturalist """ # Create an Individual that already exists in iNaturalist after a previous push nest = Individual(latitude=51.2003, longitude=4.9067, observation_time=datetime(2019, 4, 1, 10), originates_in_vespawatch=True, taxon=self.vv_taxon) nest.save() nest.delete( ) # When the observation is deleted, it is deleted without creating a InatObsToDelete object self.assertEqual(len(InatObsToDelete.objects.all()), 0) # Now a sync is called. Since there are no InatObsToDelete, the delete_mock is never called call_command('inaturalist_sync') self.delete_mock.assert_not_called()
def test_sync_push_deleted_obs_doesnt_exist(self): """ Pyinaturalist will raise a ObservationNotFound exception when you try to delete an observation that does not exist on iNaturalist. Make sure we catch that and properly handle it (delete the observation locally since it's already gone on iNaturalist) """ # Create an Individual that already exists in iNaturalist after a previous push ind = Individual(inaturalist_id=30, latitude=51.2003, longitude=4.9067, observation_time=datetime(2019, 4, 1, 10), originates_in_vespawatch=True, taxon=self.vv_taxon) ind.save() # Now delete it ind.delete() self.assertEqual(len(InatObsToDelete.objects.all()), 1) # Set a return value for the self.get_all_mock. It should return no observations self.get_all_mock.return_value = [] # Set a side effect for the self.get_obs_mock. It should raise a ObservationNotFound exception self.delete_mock.side_effect = ObservationNotFound() # Run inaturalist sync. call_command('inaturalist_sync') self.assertEqual(len(InatObsToDelete.objects.all()), 0) # TODO add tests for pulling and checking taxon id (first community taxon, then taxon) # TODO check update description # TODO check update date # TODO check update taxon
def test_sync_push_deleted_indiv(self): """ An observation that was created in vespawatch and deleted in vespawatch after it was synced, should be deleted on iNaturalist """ # Create an Individual that already exists in iNaturalist after a previous push ind = Individual(inaturalist_id=30, latitude=51.2003, longitude=4.9067, observation_time=datetime(2019, 4, 1, 10), originates_in_vespawatch=True, taxon=self.vv_taxon) ind.save() ind.delete( ) # When the observation is deleted, it is actually added to the InatObsToDelete table self.assertEqual(len(InatObsToDelete.objects.all()), 1) self.assertEqual(InatObsToDelete.objects.all()[0].inaturalist_id, 30) # Now a sync is called. The InatObsToDelete observation is deleted on iNaturalist and removed from the db call_command('inaturalist_sync') self.delete_mock.assert_called_once_with(observation_id=30, access_token='TESTTOKEN')