|
24 | 24 | from django.contrib.auth.models import AbstractBaseUser |
25 | 25 | from django.contrib.auth.models import AnonymousUser |
26 | 26 | from django.contrib.auth.models import UserManager |
| 27 | +from django.contrib.sessions.base_session import AbstractBaseSession |
27 | 28 | from django.core import validators |
28 | 29 | from django.core.exceptions import ObjectDoesNotExist |
29 | 30 | from django.core.exceptions import ValidationError |
|
35 | 36 | from morango.models import Certificate |
36 | 37 | from morango.models import SyncableModel |
37 | 38 | from morango.models import SyncableModelManager |
| 39 | +from morango.models import SyncableModelQuerySet |
| 40 | +from morango.models import UUIDField |
38 | 41 | from mptt.models import TreeForeignKey |
39 | 42 |
|
40 | 43 | from .constants import collection_kinds |
|
78 | 81 | from kolibri.core.errors import KolibriValidationError |
79 | 82 | from kolibri.core.fields import DateTimeTzField |
80 | 83 | from kolibri.core.fields import JSONField |
| 84 | +from kolibri.core.utils.model_router import KolibriModelRouter |
81 | 85 | from kolibri.core.utils.validators import JSON_Schema_Validator |
| 86 | +from kolibri.deployment.default.sqlite_db_names import SESSIONS |
82 | 87 | from kolibri.plugins.app.utils import interface |
83 | 88 | from kolibri.utils.time_utils import local_now |
84 | 89 |
|
85 | 90 | logger = logging.getLogger(__name__) |
86 | 91 |
|
| 92 | + |
| 93 | +class Session(AbstractBaseSession): |
| 94 | + """ |
| 95 | + Custom session model with user_id tracking for session management. |
| 96 | + Inherits from Django's AbstractBaseSession and adds user_id field. |
| 97 | + """ |
| 98 | + |
| 99 | + user_id = UUIDField(blank=True, null=True, db_index=True) |
| 100 | + |
| 101 | + @classmethod |
| 102 | + def get_session_store_class(cls): |
| 103 | + from .backends import SessionStore |
| 104 | + |
| 105 | + return SessionStore |
| 106 | + |
| 107 | + @classmethod |
| 108 | + def delete_all_sessions(cls, user_ids): |
| 109 | + store_class = cls.get_session_store_class() |
| 110 | + store_class.delete_all_sessions(user_ids) |
| 111 | + logger.info("Deleted all sessions for user IDs: {}".format(user_ids)) |
| 112 | + |
| 113 | + |
| 114 | +class SessionRouter(KolibriModelRouter): |
| 115 | + """ |
| 116 | + Determine how to route database calls for custom Session model. |
| 117 | + """ |
| 118 | + |
| 119 | + MODEL_CLASSES = {Session} |
| 120 | + DB_NAME = SESSIONS |
| 121 | + |
| 122 | + |
87 | 123 | DEMOGRAPHIC_FIELDS_KEY = "demographic_fields" |
88 | 124 |
|
89 | 125 |
|
@@ -631,7 +667,43 @@ def filter_readable(self, queryset): |
631 | 667 | return queryset.none() |
632 | 668 |
|
633 | 669 |
|
634 | | -class FacilityUserModelManager(SyncableModelManager, UserManager): |
| 670 | +class FacilityUserQuerySet(SyncableModelQuerySet): |
| 671 | + def update(self, **kwargs): |
| 672 | + # Check if date_deleted is being set to a non-null value (soft delete) |
| 673 | + user_ids = [] |
| 674 | + if "date_deleted" in kwargs and kwargs["date_deleted"] is not None: |
| 675 | + # Get user IDs that are currently not soft deleted |
| 676 | + user_ids = list( |
| 677 | + self.filter(date_deleted__isnull=True).values_list("id", flat=True) |
| 678 | + ) |
| 679 | + |
| 680 | + # Perform the update |
| 681 | + result = super().update(**kwargs) |
| 682 | + |
| 683 | + # Clean up sessions for soft deleted users |
| 684 | + if user_ids: |
| 685 | + Session.delete_all_sessions(user_ids) |
| 686 | + |
| 687 | + return result |
| 688 | + |
| 689 | + def delete(self): |
| 690 | + # Get user IDs before deletion for session cleanup |
| 691 | + user_ids = list(self.values_list("id", flat=True)) |
| 692 | + |
| 693 | + # Perform the deletion |
| 694 | + result = super().delete() |
| 695 | + |
| 696 | + # Clean up sessions after successful deletion |
| 697 | + Session.delete_all_sessions(user_ids) |
| 698 | + return result |
| 699 | + |
| 700 | + |
| 701 | +class BaseFacilityUserModelManager(SyncableModelManager, UserManager): |
| 702 | + def get_queryset(self): |
| 703 | + return FacilityUserQuerySet(self.model, using=self._db) |
| 704 | + |
| 705 | + |
| 706 | +class FacilityUserModelManager(BaseFacilityUserModelManager): |
635 | 707 | def get_queryset(self): |
636 | 708 | return super().get_queryset().filter(date_deleted__isnull=True) |
637 | 709 |
|
@@ -718,7 +790,7 @@ def get_or_create_os_user(self, auth_token, facility=None): |
718 | 790 | return user |
719 | 791 |
|
720 | 792 |
|
721 | | -class SoftDeletedFacilityUserModelManager(SyncableModelManager, UserManager): |
| 793 | +class SoftDeletedFacilityUserModelManager(BaseFacilityUserModelManager): |
722 | 794 | """ |
723 | 795 | Custom manager for FacilityUser that only returns users who have a non-NULL value in their date_deleted field. |
724 | 796 | """ |
@@ -773,7 +845,7 @@ def validate_role_kinds(kinds): |
773 | 845 | return kinds |
774 | 846 |
|
775 | 847 |
|
776 | | -class AllObjectsFacilityUserModelManager(SyncableModelManager, UserManager): |
| 848 | +class AllObjectsFacilityUserModelManager(BaseFacilityUserModelManager): |
777 | 849 | def get_queryset(self): |
778 | 850 | return super(AllObjectsFacilityUserModelManager, self).get_queryset() |
779 | 851 |
|
@@ -804,7 +876,7 @@ class FacilityUser(AbstractBaseUser, KolibriBaseUserMixin, AbstractFacilityDataM |
804 | 876 |
|
805 | 877 | all_objects = AllObjectsFacilityUserModelManager() |
806 | 878 | objects = FacilityUserModelManager() |
807 | | - |
| 879 | + syncing_objects = BaseFacilityUserModelManager() |
808 | 880 | soft_deleted_objects = SoftDeletedFacilityUserModelManager() |
809 | 881 |
|
810 | 882 | USERNAME_FIELD = "username" |
@@ -1061,6 +1133,28 @@ def __str__(self): |
1061 | 1133 | user=self.full_name or self.username, facility=self.facility |
1062 | 1134 | ) |
1063 | 1135 |
|
| 1136 | + def save(self, *args, **kwargs): |
| 1137 | + # Call the parent save method first |
| 1138 | + result = super().save(*args, **kwargs) |
| 1139 | + |
| 1140 | + # Clean up sessions after successful save if user is deleted |
| 1141 | + # This handles both local deletions and deletions synced via Morango |
| 1142 | + # (Morango creates new instances from deserialized data, so we can't |
| 1143 | + # reliably track changes - just clean up whenever date_deleted is set) |
| 1144 | + if self.date_deleted is not None and self.pk is not None: |
| 1145 | + Session.delete_all_sessions([self.id]) |
| 1146 | + |
| 1147 | + return result |
| 1148 | + |
| 1149 | + def delete(self, *args, **kwargs): |
| 1150 | + """ |
| 1151 | + Override delete to ensure sessions are cleaned up during hard delete. |
| 1152 | + """ |
| 1153 | + user_id = self.id |
| 1154 | + result = super().delete(*args, **kwargs) |
| 1155 | + Session.delete_all_sessions([user_id]) |
| 1156 | + return result |
| 1157 | + |
1064 | 1158 | def has_perm(self, perm, obj=None): |
1065 | 1159 | # ensure the superuser has full access to the Django admin |
1066 | 1160 | if self.is_superuser: |
|
0 commit comments