Repository: learningequality/kolibri

Issue:
detect and warn about unsupported browsers
There are a few browsers that we explicitly do not support. Currently, many of these browsers leave the user with a "broken" experience - for example a loading screen that never goes away.

Instead, we should detect browsers that are not supported and show an explicit message with instructions about what to do.

For example, if we detect that the user is on IE10, we should instruct them to use Firefox. The instructions might vary depending on detected OS (e.g. unsupported browsers on Android might have different recommended next-steps)

We should also update the [`browserlist`](https://github.com/learningequality/kolibri/blob/develop/browserslist) file to accurately reflect our supported browsers.

nice-to-have: also detect when javascript is turned off and warn about that, too


Discussion:
Potential solution. 

Consensus is to display this if the user is using IE 10 or any version below that. 

Kolibri still "kind of works" on other browsers like iOS & Safari, but we are not addressing those experiences at this time since it's a more pressing issue that the user is left on e.g. an infinite loading screen on IE.

<img width="1145" alt="screen shot 2017-11-02 at 5 28 29 pm" src="https://user-images.githubusercontent.com/6668144/32356567-4b1ddcb4-bff3-11e7-96e0-771ad556b3d7.png">


Resolving patch:
diff --git a/kolibri/core/context_processors/custom_context_processor.py b/kolibri/core/context_processors/custom_context_processor.py
--- a/kolibri/core/context_processors/custom_context_processor.py
+++ b/kolibri/core/context_processors/custom_context_processor.py
@@ -2,7 +2,50 @@
 
 from django.conf import settings
 from kolibri.auth.api import SessionViewSet
+from user_agents import parse
 
 
 def return_session(request):
     return {'session': json.dumps(SessionViewSet().get_session(request)), 'kolibri': settings.KOLIBRI_CORE_JS_NAME}
+
+
+browser_requirements = [
+    {
+        'family': 'IE',
+        'major_version': 11,
+    },
+    {
+        'family': 'Mobile Safari',
+        'blacklist': True,
+    },
+    {
+        'family': 'Android',
+        'major_version': 4,
+        'minor_version': 0,
+        'patch_version': 2,
+    },
+]
+
+def pass_browser_entry(agent, entry):
+    if agent.browser.family == entry['family']:
+        if 'blacklist' in entry and entry['blacklist']:
+            return False
+        if 'major_version' in entry:
+            major_ok = agent.browser.version[0] >= entry['major_version']
+            if 'minor_version' in entry:
+                minor_ok = agent.browser.version[1] >= entry['minor_version']
+                if 'patch_version' in entry:
+                    patch_ok = agent.browser.version[2] >= entry['patch_version']
+                    return major_ok and minor_ok and patch_ok
+                return major_ok and minor_ok
+            return major_ok
+    return True
+
+
+def supported_browser(request):
+    if 'supported_browser' not in request.session:
+
+        user_agent = parse(request.META.get('HTTP_USER_AGENT', ''))
+        request.session['supported_browser'] = all(
+            pass_browser_entry(user_agent, entry) for entry in browser_requirements)
+    return {'supported_browser': request.session['supported_browser']}
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
@@ -105,6 +105,7 @@
                 'django.contrib.auth.context_processors.auth',
                 'django.contrib.messages.context_processors.messages',
                 'kolibri.core.context_processors.custom_context_processor.return_session',
+                'kolibri.core.context_processors.custom_context_processor.supported_browser',
             ],
         },
     },


Test patch:
diff --git a/kolibri/core/test/test_context_processor.py b/kolibri/core/test/test_context_processor.py
--- a/kolibri/core/test/test_context_processor.py
+++ b/kolibri/core/test/test_context_processor.py
@@ -2,6 +2,9 @@
 
 from django.template import Template, RequestContext
 from rest_framework.test import APITestCase
+from unittest import TestCase
+
+from kolibri.core.context_processors.custom_context_processor import pass_browser_entry
 
 import json
 import mock
@@ -12,6 +15,7 @@ class ContextProcessorTestCase(APITestCase):
     def setUp(self):
         self.request = mock.Mock()
         self.request.session = {}
+        self.request.META = {}
         self.template = Template("My name is...")
         self.context = RequestContext(self.request)
 
@@ -24,3 +28,95 @@ def tearDown(self):
         self.request = None
         self.template = None
         self.context = None
+
+
+class BrowserEntryTestCase(TestCase):
+    def test_pass_browser_entry_patch_fail(self):
+        user_agent = mock.MagicMock()
+        browser_mock = mock.MagicMock()
+        browser_mock.family = 'Android'
+        browser_mock.version = (4, 0, 1, )
+        user_agent.browser = browser_mock
+        entry = {
+            'family': 'Android',
+            'major_version': 4,
+            'minor_version': 0,
+            'patch_version': 2,
+        }
+        self.assertFalse(pass_browser_entry(user_agent, entry))
+
+    def test_pass_browser_entry_patch_pass(self):
+        user_agent = mock.MagicMock()
+        browser_mock = mock.MagicMock()
+        browser_mock.family = 'Android'
+        browser_mock.version = (4, 0, 2, )
+        user_agent.browser = browser_mock
+        entry = {
+            'family': 'Android',
+            'major_version': 4,
+            'minor_version': 0,
+            'patch_version': 2,
+        }
+        self.assertTrue(pass_browser_entry(user_agent, entry))
+
+    def test_pass_browser_entry_minor_fail(self):
+        user_agent = mock.MagicMock()
+        browser_mock = mock.MagicMock()
+        browser_mock.family = 'Android'
+        browser_mock.version = (4, 0, 1)
+        user_agent.browser = browser_mock
+        entry = {
+            'family': 'Android',
+            'major_version': 4,
+            'minor_version': 1,
+        }
+        self.assertFalse(pass_browser_entry(user_agent, entry))
+
+    def test_pass_browser_entry_minor_pass(self):
+        user_agent = mock.MagicMock()
+        browser_mock = mock.MagicMock()
+        browser_mock.family = 'Android'
+        browser_mock.version = (4, 1, 1)
+        user_agent.browser = browser_mock
+        entry = {
+            'family': 'Android',
+            'major_version': 4,
+            'minor_version': 1,
+        }
+        self.assertTrue(pass_browser_entry(user_agent, entry))
+
+    def test_pass_browser_entry_major_fail(self):
+        user_agent = mock.MagicMock()
+        browser_mock = mock.MagicMock()
+        browser_mock.family = 'Android'
+        browser_mock.version = (4, 0, 1)
+        user_agent.browser = browser_mock
+        entry = {
+            'family': 'Android',
+            'major_version': 5,
+        }
+        self.assertFalse(pass_browser_entry(user_agent, entry))
+
+    def test_pass_browser_entry_major_pass(self):
+        user_agent = mock.MagicMock()
+        browser_mock = mock.MagicMock()
+        browser_mock.family = 'Android'
+        browser_mock.version = (5, 0, 1)
+        user_agent.browser = browser_mock
+        entry = {
+            'family': 'Android',
+            'major_version': 5,
+        }
+        self.assertTrue(pass_browser_entry(user_agent, entry))
+
+    def test_pass_browser_entry_black_fail(self):
+        user_agent = mock.MagicMock()
+        browser_mock = mock.MagicMock()
+        browser_mock.family = 'Android'
+        browser_mock.version = (5, 0, 1)
+        user_agent.browser = browser_mock
+        entry = {
+            'family': 'Android',
+            'blacklist': True,
+        }
+        self.assertFalse(pass_browser_entry(user_agent, entry))