Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
niconoe committed Oct 18, 2024
1 parent 98f51c5 commit 4066f08
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 99 deletions.
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,21 @@ See [INSTALL.md](INSTALL.md) for more information.

- LIFE RIPARIAS Early Alert: [production](https://alert.riparias.be) / [development](https://dev-alert.riparias.be) (Targets riparian invasive species in Belgium)
- [GBIF Alert demo instance](https://gbif-alert-demo.thebinaryforest.net/) (Always in sync with the `devel` branch of this repository)
- The Belgian Biodiversity Platform uses GBIF alert under the hood as an API for the ManaIAS project.
- The Belgian Biodiversity Platform uses GBIF alert under the hood as an API for the ManaIAS project.


# TODO before confidently update:
- think!! seen/unseen: what happens if the observation is migrated (with its seen/unseen status), but the actual criteria have changed because species has changed, for example.
- is there a check that they are automatically marked as seen if old and/or not part of any alert?
- check TODO in code (missing tests, etc)
- reread all code from branch
- test the mark as read with redis queue
- take a dump from prod DB, rerun and test all locally
- check if the user can configure its "automatically seen" delay

# TODO for data migration:
- migrate
- run python manage.py prepare_new_seen_unseen



2 changes: 1 addition & 1 deletion dashboard/management/commands/import_observations.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ def batch_insert_observations(self, observations_to_insert: list[Observation]):
replaced = obs.migrate_linked_entities()
if not replaced:
# That's a new observation in the system, it should be marked as unseen for every user
obs.mark_as_unseen_for_all_users_if_recent()
obs.mark_as_unseen_for_all_users_if_needed()

def add_arguments(self, parser: CommandParser) -> None:
parser.add_argument(
Expand Down
38 changes: 38 additions & 0 deletions dashboard/management/commands/migrate_new_seen_unseen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# One-shot command to migrate the new unseen observations to the new mechanism
# (was hard to do in a data migration)

import datetime

from django.core.management.base import BaseCommand
from django.db import IntegrityError
from django.utils import timezone

from dashboard.models import Observation, User, ObservationUnseen

one_year_ago = (timezone.now() - datetime.timedelta(days=365)).date()


class Command(BaseCommand):
def handle(self, *args, **options):
all_users = User.objects.all()

for user in all_users:
self.stdout.write(
f"{str(datetime.datetime.now())} - Migrating data for user: "
+ user.username
)

# We only migrate that for unseen observations, pertaining to user alers and that are less than a year old
for alert in user.alert_set.all():
for obs in alert.observations():
if obs.date > one_year_ago:
if not obs.observationview_set.filter(user=user).exists():
try:
ObservationUnseen.objects.create(
user=user, observation_id=obs.id
)
except IntegrityError:
# The same user might have already seen the observation from another alert
pass

self.stdout.write("Done")
66 changes: 0 additions & 66 deletions dashboard/migrations/0019_migrate_obs_views.py

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Generated by Django 4.2.13 on 2024-10-10 12:32
# Generated by Django 4.2.13 on 2024-10-10 13:20

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("dashboard", "0019_migrate_obs_views"),
("dashboard", "0018_observationunseen"),
]

operations = [
Expand Down
56 changes: 50 additions & 6 deletions dashboard/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ class User(AbstractUser):
# considered already seen.
notification_delay_days = models.IntegerField(default=365)

def obs_match_alerts(self, obs: "Observation") -> bool:
"""Return True if the observation matches at least one of the user's alerts
# TODO: test this
"""
for alert in self.alert_set.all():
if obs in alert.observations():
return True
return False

def get_language(self) -> str:
# Use this method instead of self.language to get the language code (some got en-us as a default value, that will cause issues)
if self.language == "en-us":
Expand Down Expand Up @@ -378,20 +387,23 @@ def set_or_migrate_initial_data_import(
else:
self.initial_data_import = replaced_observation.initial_data_import

def considered_seen_delay_by(self, user: WebsiteUser) -> bool:
@staticmethod
def date_older_than_user_delay(user: WebsiteUser, the_date) -> bool:
# TODO: test this logic !!
today = timezone.now().date()

return self.date < (
return the_date < (
today - datetime.timedelta(days=user.notification_delay_days)
)

def mark_as_unseen_for_all_users_if_recent(self) -> None:
def mark_as_unseen_for_all_users_if_needed(self) -> None:
"""Mark the observation as unseen for all users"""

# TODO: test this logic !!
for user in get_user_model().objects.all():
if not self.considered_seen_delay_by(user):
if not self.date_older_than_user_delay(
user, the_date=self.date
) and user.obs_match_alerts(self):
self.mark_as_unseen_by(user)

def migrate_linked_entities(self) -> bool:
Expand All @@ -402,7 +414,7 @@ def migrate_linked_entities(self) -> bool:
Returns True if it migrated an existing observation, False otherwise
Note: in case an Unseen object isn't relevant anymore (because the observation
is too old), it will be deleted rather than migrated
is too old, or it does not belong to an alert), it will be deleted rather than migrated
"""
replaced_observation = self.replaced_observation
if replaced_observation is not None:
Expand All @@ -413,7 +425,9 @@ def migrate_linked_entities(self) -> bool:
# 2. Migrating seen/unseen status
for observation_unseen in replaced_observation.observationunseen_set.all():
# TODO: extensively test this
if self.considered_seen_delay_by(observation_unseen.user):
if not observation_unseen.relevant_for_user(
date_new_observation=self.date
):
observation_unseen.delete()
else:
observation_unseen.observation = self
Expand Down Expand Up @@ -668,6 +682,20 @@ class ObservationUnseen(models.Model):
observation = models.ForeignKey(Observation, on_delete=models.CASCADE)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)

def relevant_for_user(self, date_new_observation) -> bool:
"""Return True if the observation is still relevant for the user"""
# TODO: test this
# return self.observation.considered_seen_delay_by(
# self.user
# ) or not self.user.obs_match_alerts(self.observation)

if self.observation.date_older_than_user_delay(
self.user, the_date=date_new_observation
):
return False
else:
return self.user.obs_match_alerts(self.observation)

class Meta:
unique_together = [
("observation", "user"),
Expand Down Expand Up @@ -788,7 +816,23 @@ def as_dict(self) -> dict[str, Any]:
"emailNotificationsFrequency": self.email_notifications_frequency,
}

def observations(self) -> QuerySet[Observation]:
"""Return all observations matching this alert"""
# TODO: test this
# TODO: check if used
return Observation.objects.filtered_from_my_params(
species_ids=[s.pk for s in self.species.all()],
datasets_ids=[d.pk for d in self.datasets.all()],
areas_ids=[a.pk for a in self.areas.all()],
start_date=None,
end_date=None,
initial_data_import_ids=[],
status_for_user=None,
user=self.user,
)

def unseen_observations(self) -> QuerySet[Observation]:
"""Return all unseen observations matching this alert"""
return Observation.objects.filtered_from_my_params(
species_ids=[s.pk for s in self.species.all()],
datasets_ids=[d.pk for d in self.datasets.all()],
Expand Down
Binary file modified dashboard/tests/commands/sample_data/gbif_download.zip
Binary file not shown.
Loading

0 comments on commit 4066f08

Please sign in to comment.