Repository: learningequality/kolibri

Issue:
Up the login session idle time to 10 minutes
## Summary

Currently at 5 minutes, I find it to be very low. But I'm not a learner, I'm a person who has a lot of tabs opened while testing :)

This is targeted already at 0.4 as a quick fix kind of thing.

Suggesting to up it to 10 minutes. Would like to hear what you think about this @harishbalachandran ?


Discussion:
@harishbalachandran if you find any issues with the timeout, please report here!
Some additional context from a slack conversation:

![image](https://user-images.githubusercontent.com/2367265/31357352-39cf4a48-acf6-11e7-9a43-dbed85b4e72d.png)

However it sounds like @benjaoming timed out even though he had the page open, so the heartbeat wasn't working?


Yeah, I think I misdescribed the precise heartbeat behaviour there, and was focusing more on the 'passive use' case.

Heartbeat only beats if the user has interacted with the UI at all.
Hm okay then I'd definitely be worried about people being logged out too soon - it's much more aggressive than 'end session when the browser is closed'.

What about implementing a light-weight session keep-alive heartbeat? No DB interaction - just to make this feature work as originally intended.

Yeah, could make an endpoint with the active flag that would only update the UserSessionLog if true.
@indirectlylit yup it's the *session ***idle*** time* - I don't know how to phrase it any better. But you have to leave the tab or window with no interaction. I think this is likely to occur when learners are listening to instructions from the teacher.. 5 minutes aren't long. Of course we want to log users out so learners cannot (intentionally/mistakenly) take over each other's profiles.

An example:

\- Teacher: Ahrem, everyone put down your tablets for a moment, I wanna tell you about how life was like before computers
\- Teacher: *(talks about forests and animals)*
\- Teacher: Alright, now back to the assignments
\- \**(everybody's logged out)*\*
@benjaoming @rtibbles @indirectlylit, the main reason for a session log out was that, when a student does not log out on a particular tablet and the next learner gets the same tablet in the next session then he/she is logged in to the previous leaners account. 

But like @benjaoming pointed out, there are use cases when a teacher would request the learners to pause and explain a concept or a child might pause from his current work, help his/her friends to clear some doubts.

Since the logout uses case cannot be implemented when a learner closes the browser or when a tablet is turned off, it might be worth while to keep 10 minutes as a logout time rather than 5 minutes. 
Reason being:
* Even if the leaners are sharing the same tablets for the next session, it takes more than 10 minutes before they use the same tablets again in a different tablet class in an another division or grade. 
* 10 minutes seams to be a good time before the learner gets back to his or her screen and continue where they paused. If the idle time is more than 10 mins its okey for the leaner to be logged out, but there has to be an automatic redirection where in the leaner is logged out and is given the home page clearly indicating that he needs to log out. In todays testing, the 5 minute session time out implemented on 0.4.6 does not happen smoothly and the leaner is shown an error page. @swapnil106111 will be logging a separate issues to highlight error sate while the tablet is kept idle for 5 minutes.
Thanks @harishbalachandran for the thorough feedback! If you have any extra detailed adjustments - like say 7 or 8 minutes, that's also interesting! 5 and 10 are nice because they are easy to remember and communicate.

>  In todays testing, the 5 minutes session time out implemented on 0.4.6 does not happen smoothly and the leaner is shown an error page, rather than logging out and shown the homepage that would indicate that the session has times out. @swapnil106111 will be logging a separate issues to highlight error sate while the tablet is kept idle for 5 minutes.

This should be  #2390!

to be concrete, my suggestion above was:

* keep the server-side session timeout at 5 minutes
* implement an ajax 'ping' on a javascript timer, maybe every minute
* when a browser window is open - even in the background - this should prevent the session from timing out, ever
* when the browser window is closed for more than 5 minutes, the session times out


Resolving patch:
diff --git a/kolibri/auth/api.py b/kolibri/auth/api.py
--- a/kolibri/auth/api.py
+++ b/kolibri/auth/api.py
@@ -263,3 +263,9 @@ def get_session(self, request):
             UserSessionLog.update_log(user)
 
             return session
+
+
+class KeepSessionAliveViewSet(viewsets.ViewSet):
+
+    def retrieve(self, request, pk=None):
+        return Response()
diff --git a/kolibri/auth/api_urls.py b/kolibri/auth/api_urls.py
--- a/kolibri/auth/api_urls.py
+++ b/kolibri/auth/api_urls.py
@@ -2,7 +2,7 @@
 
 from .api import (
     ClassroomViewSet, CurrentFacilityViewSet, DeviceOwnerViewSet, FacilityDatasetViewSet, FacilityUsernameViewSet, FacilityUserViewSet, FacilityViewSet,
-    LearnerGroupViewSet, MembershipViewSet, RoleViewSet, SessionViewSet, SignUpViewSet
+    KeepSessionAliveViewSet, LearnerGroupViewSet, MembershipViewSet, RoleViewSet, SessionViewSet, SignUpViewSet
 )
 
 router = routers.SimpleRouter()
@@ -19,5 +19,6 @@
 router.register(r'classroom', ClassroomViewSet, base_name='classroom')
 router.register(r'learnergroup', LearnerGroupViewSet, base_name='learnergroup')
 router.register(r'signup', SignUpViewSet, base_name='signup')
+router.register(r'keepalive', KeepSessionAliveViewSet, base_name='keepalive')
 
 urlpatterns = router.urls
diff --git a/kolibri/deployment/default/settings/base.py b/kolibri/deployment/default/settings/base.py
--- a/kolibri/deployment/default/settings/base.py
+++ b/kolibri/deployment/default/settings/base.py
@@ -322,4 +322,4 @@
 
 SESSION_EXPIRE_AT_BROWSER_CLOSE = True
 
-SESSION_COOKIE_AGE = 300
+SESSION_COOKIE_AGE = 600


Test patch:
diff --git a/kolibri/core/assets/test/heartbeat.js b/kolibri/core/assets/test/heartbeat.js
--- a/kolibri/core/assets/test/heartbeat.js
+++ b/kolibri/core/assets/test/heartbeat.js
@@ -45,7 +45,7 @@ describe('HeartBeat', function () {
     it('should call the setInactive method', function () {
       const spy = sinon.spy(HeartBeat.prototype, 'setInactive');
       this.test = new HeartBeat({});
-      assert.ok(spy.calledTwice);
+      assert.ok(spy.calledOnce);
       spy.restore();
     });
     it('should call the start method', function () {
@@ -59,11 +59,33 @@ describe('HeartBeat', function () {
     beforeEach(function () {
       this.heartBeat = new HeartBeat({});
       this.heartBeat.active = false;
+      this.sessionfetchspy = sinon.stub();
+      this.sessionfetchspy.returns(Promise.resolve({}));
+      this.keepalivefetchspy = sinon.stub();
+      this.keepalivefetchspy.returns(Promise.resolve({}));
+      this.sessionModelMock = {
+        fetch: this.sessionfetchspy,
+      };
+      this.keepaliveModelMock = {
+        fetch: this.keepalivefetchspy,
+      };
+      this.kolibri = {
+        resources: {
+          SessionResource: {
+            getModel: () => this.sessionModelMock,
+          },
+          KeepAliveResource: {
+            getModel: () => this.keepaliveModelMock,
+          },
+        },
+      };
+      this.heartBeat.kolibri = this.kolibri;
     });
     it('should call setInactive', function () {
       const spy = sinon.spy(this.heartBeat, 'setInactive');
       this.heartBeat.beat();
       assert.ok(spy.calledOnce);
+      spy.restore();
     });
     it('should set timerId to a setTimeout identifier', function () {
       this.heartBeat.beat();
@@ -72,26 +94,17 @@ describe('HeartBeat', function () {
     it('should return a setTimeout identifier', function () {
       assert.equal(typeof this.heartBeat.beat(), 'number');
     });
+    it('should ping KeepAliveResource', function () {
+      this.heartBeat.beat();
+      assert.ok(this.keepalivefetchspy.calledOnce);
+    });
     describe('and activity is detected', function () {
       beforeEach(function () {
         this.heartBeat.active = true;
-        this.fetchspy = sinon.stub();
-        this.fetchspy.returns(Promise.resolve({}));
-        this.modelMock = {
-          fetch: this.fetchspy,
-        };
-        this.kolibri = {
-          resources: {
-            SessionResource: {
-              getModel: () => this.modelMock,
-            },
-          },
-        };
-        this.heartBeat.kolibri = this.kolibri;
       });
       it('should call fetch on the session model', function () {
         this.heartBeat.beat();
-        assert.ok(this.fetchspy.calledOnce);
+        assert.ok(this.sessionfetchspy.calledOnce);
       });
       it('should call setActivityListeners', function () {
         const spy = sinon.spy(this.heartBeat, 'setActivityListeners');