Skip to content

Conversation

rtibbles
Copy link
Member

Summary

  • Abstracts our existing database routers into a base class and reuses it in all existing implementations - cleaning up unnecessary code in the process
  • Migrates Kolibri from file based sessions to database based sessions using a custom model and backend based on Django CachedDB in order to track the user id on the session model
  • For SQLite backends, it adds an additional database to limit the additional bottleneck that using the database backend might add
  • Adds logic to the queryset and model for FacilityUser to ensure that any time a user is deleted or soft deleted, we also clear any associated sessions
  • Cleans up previous API specific logic in favour of this approach
  • Adds tests for the new model and queryset updates

References

FIxes #13542

Note that this reverts #3944 which was originally implemented for a very modest performance improvement. I think the combination of a wide range of other performance improvements, plus the use of a separate database in this implementation means that any performance degradation from changing back here should be negligible.

Reviewer guidance

I used Claude Code to do the initial model and queryset updates and write tests for it, and then heavily edited it to reduce code verbosity, fix errors and ensure the tests were asserting the right things. All other code was hand written.

Both the deletion flows in the issue itself, when deleting in an LoD and also when deleting using the new bulk user management flows should work here. Also, if a user is deleted due to a facility being deleted from a device that should also work.

The result here should be that the user is logged out identically if the user's session timed out.

Update all implementations.
Remove redundant unregistering of NotificationsRouter.
To allow sessions to be cleared via user id.
Use separate sessions database to prevent bottlenecks.
@github-actions github-actions bot added DEV: backend Python, databases, networking, filesystem... SIZE: large labels Sep 18, 2025
Copy link
Contributor

github-actions bot commented Sep 18, 2025

@nucleogenesis nucleogenesis self-assigned this Sep 18, 2025
@github-actions github-actions bot added APP: Device Re: Device App (content import/export, facility-syncing, user permissions, etc.) APP: Facility Re: Facility App (user/class management, facility settings, csv import/export, etc.) APP: Learn Re: Learn App (content, quizzes, lessons, etc.) APP: Coach Re: Coach App (lessons, quizzes, groups, reports, etc.) labels Sep 18, 2025
Copy link
Member

@nucleogenesis nucleogenesis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left some questions / thoughts - nothing blocking or urgent to reply to.

Overall I think that I got a good sense of how the changes work. The Session* classes were a bit weird to grok at first but seems the crux is in customizing the SessionStore so you can override create_model_instance to handle the user_id business. The rest then seems like glue code w/ the new Session model and the DB.

I didn't spin it up yet but don't want to forget to submit this review so I'll come back for further review after some manual testing.

Comment on lines +34 to +38
def cleanup_legacy_file_sessions():
"""
Clean up legacy file-based sessions when upgrading to database-backed sessions.
Removes the sessions directory from KOLIBRI_HOME if it exists.
"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<3

@@ -0,0 +1,69 @@
from abc import ABC
from abc import abstractmethod
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(non-blocking curiosity) The reason I'm seeing for using ABC/abstractmethod here is to ensure that classes inheriting this will be required to override specific methods or throw an error.

Are there any other benefits to using an abstract class or would it be functionally similar to:

def MODEL_CLASSES(self):
    raise "Not implemented"

I think this came to mind because that's how I've done abstract classes in Ruby

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love how this whole module DRYs things up

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, ABC and abstractmethods are the more explicit way to do this.

user=self.full_name or self.username, facility=self.facility
)

def __init__(self, *args, **kwargs):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(non blocking opinion) - __init__ feels like it should always be at the top of a class to me but as I type this I'm feeling like this is largely based on vibes 😆 -- I think maybe I just like to see overrides at the top of a class so I can first answer the question "what is the class doing in relation to it's parent's implementation" then the second question of "now what does this class do uniquely".

In any case - __str__ is overridden down here too so the rest fit in well enough 😄

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah - I think I have a similar intuition - the only advantage to it being here is that its closer to where the intialized value is used.

Comment on lines -20 to -26
# Remove NotificationsRouter if sqlite is not being used:
if settings.DATABASES["default"]["ENGINE"] != "django.db.backends.sqlite3":
ROUTER_ID = "kolibri.core.notifications.models.NotificationsRouter"
if ROUTER_ID in settings.DATABASE_ROUTERS:
settings.DATABASE_ROUTERS = tuple(
filter(lambda x: x != ROUTER_ID, settings.DATABASE_ROUTERS)
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice to see code like this in the red :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has been redundant for quite some time, because of the router conditionality being in settings/base.py instead!

@pcenov
Copy link
Member

pcenov commented Sep 22, 2025

Hi @rtibbles,
Unfortunately the issue reported in #13542 is still present.
Also when on device A I created a server with 2 facilities, and then on device B I went through the LOD user flow and imported a user from the 2nd facility and then deleted the entire facility the user remains signed in with no indication whatsoever that the facility has been deleted other than syncing not working anymore.
The same scenario is valid also for a single device with multiple facilities, where if I add a new facility, create and assign a learner to a class, sign in as the learner and then delete the entire facility - the learner still remains signed in.

Logs:
server-logs.zip
lod-logs.zip

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

APP: Coach Re: Coach App (lessons, quizzes, groups, reports, etc.) APP: Device Re: Device App (content import/export, facility-syncing, user permissions, etc.) APP: Facility Re: Facility App (user/class management, facility settings, csv import/export, etc.) APP: Learn Re: Learn App (content, quizzes, lessons, etc.) DEV: backend Python, databases, networking, filesystem... SIZE: large

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Manage users in LOD - Unhandled error when removing a signed-in user

3 participants