Repository: learningequality/kolibri

Issue:
UnicodeEncodeError on Windows

### Observed behavior
[Reported by @laurenlichtman](https://learningequality.slack.com/archives/C4HNGQJGH/p1576206775009700) after running the `kolibri manage listchannels` in Command Prompt on a Windows 10 device with a lot of Arabic channels. 

@indirectlylit suggests this could be straightforward to fix in Kolibri code given the #3769.
Unsure about the milestone or the priority, given our current crunch. 0.13 has this feature in the UI, so leaving it as P2 for now.

### Expected behavior
Listing and reordering channels with names in UTF8 charmap should work from the Windows console.

### User-facing consequences
Inability to use the feature from the command line.

### Errors and logs
```
[32mINFO     Option RUN_MODE in section [Deployment] being overridden by environment variable KOLIBRI_RUN_MODE[0m
[37mINFO     Running Kolibri with the following settings: kolibri.deployment.default.settings.base[0m
[37mINFO     Invoking command 'kolibri manage listchannels'.[0m
Pos       ID                                      Name
---       --                                      ----
None      f6cb302ef6594db4b4a04b4991a595c2        Plan Educativo TIC Basico
2         fdab6fb66ba24d05acd011e85bdb36ba        KICD Mathematics Curriculum (DRAFT)
12        da53f90b1be25752a04682bbc353659f        Ciencia NASA
14        0e173fca6e9052f8a474a2fb84055faf        Global Digital Library - Book Catalog
58        32b5fc156a7d46ddb8cea9663a1871be        KICD Chemistry Curriculum (DRAFT)
60        335b8e0ed8a3426580e4c58f62810d25        KICD Life Skills Curriculum (DRAFT)
62        591b7e1bc89645ef846c1685a7dd7b50        KICD Biology Curriculum (DRAFT)
64        54aa253a3266416da0c847e16e64aa7b        KICD Physics Curriculum (DRAFT)
66        e006726b1f35487eb7b2aa7cb11caf4c        Secondary School
68        8111ac9ab99646a1be9984f13b29167d        Youth Center
70        ce46decb878442a09a250c36ab50dcfc        Funza
72        18a6df18b021457daba3588584e6e4ac        Ministry of Education and Sports & Kyambogo University for Inclusive Education in Uganda
74        7ec3b2ad48925d639592954e2298618f        HP LIFE Courses (English)
Traceback (most recent call last):
  File "C:\Python34\lib\runpy.py", line 170, in _run_module_as_main
    "__main__", mod_spec)
  File "C:\Python34\lib\runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "C:\Python34\Scripts\kolibri.exe\__main__.py", line 9, in <module>
  File "C:\Python34\lib\site-packages\kolibri\utils\cli.py", line 601, in main
    manage(command, args=django_args)
  File "C:\Python34\lib\site-packages\kolibri\utils\cli.py", line 469, in manage
    execute_from_command_line(argv=argv)
  File "C:\Python34\lib\site-packages\kolibri\dist\django\core\management\__init__.py", line 364, in execute_from_command_line
    utility.execute()
  File "C:\Python34\lib\site-packages\kolibri\dist\django\core\management\__init__.py", line 356, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "C:\Python34\lib\site-packages\kolibri\dist\django\core\management\base.py", line 283, in run_from_argv
    self.execute(*args, **cmd_options)
  File "C:\Python34\lib\site-packages\kolibri\dist\django\core\management\base.py", line 330, in execute
    output = self.handle(*args, **options)
  File "C:\Python34\lib\site-packages\kolibri\core\content\management\commands\listchannels.py", line 22, in handle
    self.stdout.write(format_line(channel.order, channel.id, channel.name))
  File "C:\Python34\lib\site-packages\kolibri\dist\django\core\management\base.py", line 110, in write
    self._out.write(force_str(style_func(msg)))
  File "C:\Python34\lib\encodings\cp437.py", line 19, in encode
    return codecs.charmap_encode(input,self.errors,encoding_map)[0]
UnicodeEncodeError: 'charmap' codec can't encode characters in position 58-65: character maps to <undefined>
```

### Steps to reproduce
…

### Context
 * Kolibri version: 0.12.x
 * Operating system: Windows 10
 * Browser: -


Discussion:

Resolving patch:
diff --git a/kolibri/utils/env.py b/kolibri/utils/env.py
--- a/kolibri/utils/env.py
+++ b/kolibri/utils/env.py
@@ -11,13 +11,12 @@
     # TODO: Move version tools to build tools, so we don't have to do this
     from colorlog import ColoredFormatter
     from colorlog import getLogger
-    from colorlog import StreamHandler
 except ImportError:
-    StreamHandler = None
     getLogger = None
     ColoredFormatter = None
 
 from .logger import LOG_COLORS
+from .logger import EncodingStreamHandler as StreamHandler
 from kolibri.utils.compat import monkey_patch_collections
 
 
diff --git a/kolibri/utils/logger.py b/kolibri/utils/logger.py
--- a/kolibri/utils/logger.py
+++ b/kolibri/utils/logger.py
@@ -17,6 +17,39 @@
 }
 
 
+class EncodingStreamHandler(logging.StreamHandler):
+    """
+    A custom stream handler that encodes the log message to the specified encoding.
+    """
+
+    terminator = "\n"
+
+    def __init__(self, stream=None, encoding="utf-8"):
+        super(EncodingStreamHandler, self).__init__(stream)
+        self.encoding = encoding
+
+    def emit(self, record):
+        """
+        Vendored and modified from:
+        https://github.com/python/cpython/blob/main/Lib/logging/__init__.py#L1098
+        """
+        try:
+            msg = self.format(record)
+            stream = self.stream
+            # issue 35046: merged two stream.writes into one.
+            text = msg + self.terminator
+            if self.encoding and hasattr(stream, "buffer"):
+                bytes_to_write = text.encode(self.encoding)
+                stream.buffer.write(bytes_to_write)
+            else:
+                stream.write(text)
+            self.flush()
+        except RuntimeError:  # See issue 36272
+            raise
+        except Exception:
+            self.handleError(record)
+
+
 class KolibriTimedRotatingFileHandler(TimedRotatingFileHandler):
     """
     A custom TimedRotatingFileHandler that overrides two methods, getFilesToDelete
@@ -172,14 +205,14 @@ def get_default_logging_config(LOG_ROOT, debug=False, debug_database=False):
         "handlers": {
             "console-error": {
                 "level": "ERROR",
-                "class": "logging.StreamHandler",
+                "class": "kolibri.utils.logger.EncodingStreamHandler",
                 "formatter": "color",
                 "stream": "ext://sys.stderr",
             },
             "console": {
                 "level": DEFAULT_LEVEL,
                 "filters": ["no_exceptions"],
-                "class": "logging.StreamHandler",
+                "class": "kolibri.utils.logger.EncodingStreamHandler",
                 "formatter": "color",
                 "stream": "ext://sys.stdout",
             },
@@ -191,6 +224,7 @@ def get_default_logging_config(LOG_ROOT, debug=False, debug_database=False):
                 "formatter": "simple_date",
                 "when": "midnight",
                 "backupCount": 30,
+                "encoding": "utf-8",
             },
             "file_debug": {
                 "level": "DEBUG",
@@ -198,6 +232,7 @@ def get_default_logging_config(LOG_ROOT, debug=False, debug_database=False):
                 "class": "logging.FileHandler",
                 "filename": os.path.join(LOG_ROOT, "debug.txt"),
                 "formatter": "simple_date",
+                "encoding": "utf-8",
             },
         },
         "loggers": {


Test patch: