diff --git a/care/users/apps.py b/care/users/apps.py index 82d9c53493..3a846e9f80 100644 --- a/care/users/apps.py +++ b/care/users/apps.py @@ -7,7 +7,4 @@ class UsersConfig(AppConfig): verbose_name = _("Users") def ready(self): - try: - import care.users.signals # noqa F401 - except ImportError: - pass + import care.users.signals # noqa F401 diff --git a/care/users/migrations/0009_userfacilityallocation.py b/care/users/migrations/0009_userfacilityallocation.py new file mode 100644 index 0000000000..4411592627 --- /dev/null +++ b/care/users/migrations/0009_userfacilityallocation.py @@ -0,0 +1,70 @@ +# Generated by Django 4.2.2 on 2023-07-12 12:27 + +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +def fill_user_facility_allocation(apps, schema_editor): + UserFacilityAllocation = apps.get_model("users", "UserFacilityAllocation") + User = apps.get_model("users", "User") + users = User.objects.filter(home_facility__isnull=False) + + to_create = [ + UserFacilityAllocation( + user=user, facility=user.home_facility, start_date=user.date_joined + ) + for user in users + ] + UserFacilityAllocation.objects.bulk_create(to_create, batch_size=2000) + + +def reverse_fill_user_facility_allocation(apps, schema_editor): + UserFacilityAllocation = apps.get_model("users", "UserFacilityAllocation") + UserFacilityAllocation.objects.all().delete() + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0370_merge_20230705_1500"), + ("users", "0008_rename_skill_and_add_new_20230817_1937"), + ] + + operations = [ + migrations.CreateModel( + name="UserFacilityAllocation", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("start_date", models.DateTimeField(default=django.utils.timezone.now)), + ("end_date", models.DateTimeField(blank=True, null=True)), + ( + "facility", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="facility.facility", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.RunPython( + fill_user_facility_allocation, reverse_fill_user_facility_allocation + ), + ] diff --git a/care/users/models.py b/care/users/models.py index 571960fcfe..28ec87ed9e 100644 --- a/care/users/models.py +++ b/care/users/models.py @@ -5,6 +5,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.urls import reverse +from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ from care.utils.models.base import BaseModel @@ -359,3 +360,16 @@ def save(self, *args, **kwargs) -> None: if self.district is not None: self.state = self.district.state super().save(*args, **kwargs) + + +class UserFacilityAllocation(models.Model): + """ + This model tracks the allocation of a user to a facility for metabase analytics. + """ + + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="+") + facility = models.ForeignKey( + "facility.Facility", on_delete=models.CASCADE, related_name="+" + ) + start_date = models.DateTimeField(default=now) + end_date = models.DateTimeField(null=True, blank=True) diff --git a/care/users/signals.py b/care/users/signals.py index a60aebe134..85feb06a19 100644 --- a/care/users/signals.py +++ b/care/users/signals.py @@ -1,9 +1,15 @@ +import contextlib + from django.conf import settings from django.core.mail import EmailMessage +from django.db.models.signals import post_save, pre_save from django.dispatch import receiver from django.template.loader import render_to_string +from django.utils.timezone import now from django_rest_passwordreset.signals import reset_password_token_created +from .models import UserFacilityAllocation + @receiver(reset_password_token_created) def password_reset_token_created( @@ -40,3 +46,49 @@ def password_reset_token_created( ) msg.content_subtype = "html" # Main content is now text/html msg.send() + + +@receiver(pre_save, sender=settings.AUTH_USER_MODEL) +def save_fields_before_update(sender, instance, raw, using, update_fields, **kwargs): + if raw: + return + + if instance.pk: + fields_to_save = {"home_facility"} + if update_fields: + fields_to_save &= set(update_fields) + if fields_to_save: + with contextlib.suppress(IndexError): + instance._previous_values = instance.__class__._base_manager.filter( + pk=instance.pk + ).values(*fields_to_save)[0] + + +@receiver(post_save, sender=settings.AUTH_USER_MODEL) +def track_user_facility_allocation( + sender, instance, created, raw, using, update_fields, **kwargs +): + if raw or (update_fields and "home_facility" not in update_fields): + return + + if created and instance.home_facility: + UserFacilityAllocation.objects.create( + user=instance, facility=instance.home_facility + ) + return + + last_home_facility = getattr(instance, "_previous_values", {}).get("home_facility") + + if ( + last_home_facility and instance.home_facility_id != last_home_facility + ) or instance.deleted: + # this also includes the case when the user's new home facility is set to None + UserFacilityAllocation.objects.filter( + user=instance, facility=last_home_facility, end_date__isnull=True + ).update(end_date=now()) + + if instance.home_facility_id and instance.home_facility_id != last_home_facility: + # create a new allocation if new home facility is changed + UserFacilityAllocation.objects.create( + user=instance, facility=instance.home_facility + ) diff --git a/care/users/tests/test_user_homefacility_allocation_tracking.py b/care/users/tests/test_user_homefacility_allocation_tracking.py new file mode 100644 index 0000000000..8bf8b9539c --- /dev/null +++ b/care/users/tests/test_user_homefacility_allocation_tracking.py @@ -0,0 +1,88 @@ +from care.users.models import User, UserFacilityAllocation +from care.utils.tests.test_base import TestBase + + +class TestUserFacilityAllocation(TestBase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.new_facility = cls.create_facility(cls.district) + + @classmethod + def tearDownClass(cls): + cls.new_facility.delete() + super().tearDownClass() + + def tearDown(self): + super().tearDown() + User._base_manager.filter(username="facility_allocation_test_user").delete() + UserFacilityAllocation.objects.all().delete() + + def test_user_facility_allocation_is_created_when_user_is_created(self): + user = self.create_user( + self.district, + username="facility_allocation_test_user", + home_facility=self.facility, + ) + self.assertTrue(UserFacilityAllocation.objects.filter(user=user).exists()) + + def test_user_facility_allocation_is_ended_when_home_facility_is_cleared(self): + user = self.create_user( + self.district, + username="facility_allocation_test_user", + home_facility=self.facility, + ) + user.home_facility = None + user.save() + allocation = UserFacilityAllocation.objects.get( + user=user, facility=self.facility + ) + self.assertIsNotNone(allocation.end_date) + + def test_user_facility_allocation_is_ended_when_user_is_deleted(self): + user = self.create_user( + self.district, + username="facility_allocation_test_user", + home_facility=self.facility, + ) + user.deleted = True + user.save() + allocation = UserFacilityAllocation.objects.get( + user=user, facility=self.facility + ) + self.assertIsNotNone(allocation.end_date) + + def test_user_facility_allocation_on_home_facility_changed(self): + user = self.create_user( + self.district, + username="facility_allocation_test_user", + home_facility=self.facility, + ) + user.home_facility = self.new_facility + user.save() + allocation = UserFacilityAllocation.objects.get( + user=user, facility=self.facility + ) + self.assertIsNotNone(allocation.end_date) + self.assertTrue( + UserFacilityAllocation.objects.filter( + user=user, facility=self.new_facility + ).exists() + ) + + def test_user_facility_allocation_is_not_created_when_user_is_created_without_home_facility( + self, + ): + user = self.create_user(self.district, username="facility_allocation_test_user") + self.assertFalse(UserFacilityAllocation.objects.filter(user=user).exists()) + + def test_user_facility_allocation_is_not_changed_when_update_fields_is_passed_without_home_facility( + self, + ): + user = self.create_user( + self.district, + username="facility_allocation_test_user", + home_facility=self.facility, + ) + user.save(update_fields=["last_login"]) + self.assertEqual(UserFacilityAllocation.objects.filter(user=user).count(), 1)