prompt: You will be provided with a partial code base and an issue statement explaining a problem to resolve.
  <issue>
  Compress HTML pages in the cache
  Exercise and chapter pages are stored in the cache and only update if the backend reports that there is a change. Some pages might be large (e.g. 1M), but do compress relatively well. Memcached API supports compression on the fly, but that is not usable over django API.
  
  Thus, we should at least compress HTML content manually. Alternatively, we can specialize `CachedAbstract` for memcached, which would also allow us to use `cas` operation.
  
  Relevant files:
  * `lib/cache/cached.py`
  * `exercise/cache/exercise.py` (`content` in `_generate_data(...)` and `content()`)
  
  </issue>
  <code>
  [start of exercise/cache/exercise.py]
  1 import time
  2 from django.conf import settings
  3 from django.db.models.signals import post_save, post_delete
  4 
  5 from lib.cache import CachedAbstract
  6 from lib.remote_page import RemotePageNotModified
  7 from ..protocol.aplus import load_exercise_page
  8 
  9 
  10 class ExerciseCache(CachedAbstract):
  11     """ Exercise HTML content """
  12     KEY_PREFIX = "exercise"
  13 
  14     def __init__(self, exercise, language, request, students, url_name):
  15         self.exercise = exercise
  16         self.load_args = [language, request, students, url_name]
  17         super().__init__(exercise, modifiers=[language])
  18 
  19     def _needs_generation(self, data):
  20         expires = data['expires'] if data else None
  21         return not expires or time.time() > expires
  22 
  23     def _generate_data(self, exercise, data=None):
  24         try:
  25             page = exercise.load_page(
  26                 *self.load_args,
  27                 last_modified=data['last_modified'] if data else None
  28             )
  29             return {
  30                 'head': page.head,
  31                 'content': page.content,
  32                 'last_modified': page.last_modified,
  33                 'expires': page.expires if page.is_loaded else 0,
  34             }
  35         except RemotePageNotModified as e:
  36             if e.expires:
  37                 data['expires'] = e.expires
  38             return data
  39 
  40     def head(self):
  41         return self.data['head']
  42 
  43     def content(self):
  44         return self.data['content']
  45 
  46 
  47 def invalidate_instance(instance):
  48     for module in instance.course_modules.all():
  49         for exercise in module.learning_objects.all():
  50             for language,_ in settings.LANGUAGES:
  51                 ExerciseCache.invalidate(exercise, modifiers=[language])
  52
  [end of exercise/cache/exercise.py]
  </code>
  
  I need you to solve this issue by generating a single patch file that I can apply directly to this repository using git apply. Please respond with a single patch file in the following format.
  <patch>
  diff --git a/file.py b/file.py
  --- a/file.py
  +++ b/file.py
  @@ -1,27 +1,35 @@
   def euclidean(a, b):
  -    while b:
  -        a, b = b, a % b
  -    return a
  +    if b == 0:
  +        return a
  +    return euclidean(b, a % b)
   
   
   def bresenham(x0, y0, x1, y1):
       points = []
       dx = abs(x1 - x0)
       dy = abs(y1 - y0)
  -    sx = 1 if x0 < x1 else -1
  -    sy = 1 if y0 < y1 else -1
  -    err = dx - dy
  +    x, y = x0, y0
  +    sx = -1 if x0 > x1 else 1
  +    sy = -1 if y0 > y1 else 1
   
  -    while True:
  -        points.append((x0, y0))
  -        if x0 == x1 and y0 == y1:
  -            break
  -        e2 = 2 * err
  -        if e2 > -dy:
  -            err -= dy
  -            x0 += sx
  -        if e2 < dx:
  -            err += dx
  -            y0 += sy
  +    if dx > dy:
  +        err = dx / 2.0
  +        while x != x1:
  +            points.append((x, y))
  +            err -= dy
  +            if err < 0:
  +                y += sy
  +                err += dx
  +            x += sx
  +    else:
  +        err = dy / 2.0
  +        while y != y1:
  +            points.append((x, y))
  +            err -= dx
  +            if err < 0:
  +                x += sx
  +                err += dy
  +            y += sy
  +
  +    points.append((x, y))
       return points
  
  </patch>
golden_diff: diff --git a/exercise/cache/exercise.py b/exercise/cache/exercise.py
  --- a/exercise/cache/exercise.py
  +++ b/exercise/cache/exercise.py
  @@ -1,4 +1,6 @@
  +import logging
   import time
  +
   from django.conf import settings
   from django.db.models.signals import post_save, post_delete
   
  @@ -6,6 +8,18 @@
   from lib.remote_page import RemotePageNotModified
   from ..protocol.aplus import load_exercise_page
   
  +logger = logging.getLogger('aplus.cached')
  +
  +try:
  +    from lz4.block import compress as _compress, decompress
  +    def compress(data):
  +        return _compress(data, compression=1)
  +except ImportError:
  +    logger.warning("Unable to import lz4, using a slower zlib instead")
  +    from zlib import compress as _compress, decompress
  +    def compress(data):
  +        return _compress(data, level=1)
  +
   
   class ExerciseCache(CachedAbstract):
       """ Exercise HTML content """
  @@ -26,9 +40,12 @@
                   *self.load_args,
                   last_modified=data['last_modified'] if data else None
               )
  +
  +            content = compress(page.content.encode('utf-8'))
  +
               return {
                   'head': page.head,
  -                'content': page.content,
  +                'content': content,
                   'last_modified': page.last_modified,
                   'expires': page.expires if page.is_loaded else 0,
               }
  @@ -41,7 +58,8 @@
           return self.data['head']
   
       def content(self):
  -        return self.data['content']
  +        content = decompress(self.data['content']).decode('utf-8')
  +        return content
   
   
   def invalidate_instance(instance):
  
verification_info: {"golden_diff": "diff --git a/exercise/cache/exercise.py b/exercise/cache/exercise.py\n--- a/exercise/cache/exercise.py\n+++ b/exercise/cache/exercise.py\n@@ -1,4 +1,6 @@\n+import logging\n import time\n+\n from django.conf import settings\n from django.db.models.signals import post_save, post_delete\n \n@@ -6,6 +8,18 @@\n from lib.remote_page import RemotePageNotModified\n from ..protocol.aplus import load_exercise_page\n \n+logger = logging.getLogger('aplus.cached')\n+\n+try:\n+    from lz4.block import compress as _compress, decompress\n+    def compress(data):\n+        return _compress(data, compression=1)\n+except ImportError:\n+    logger.warning(\"Unable to import lz4, using a slower zlib instead\")\n+    from zlib import compress as _compress, decompress\n+    def compress(data):\n+        return _compress(data, level=1)\n+\n \n class ExerciseCache(CachedAbstract):\n     \"\"\" Exercise HTML content \"\"\"\n@@ -26,9 +40,12 @@\n                 *self.load_args,\n                 last_modified=data['last_modified'] if data else None\n             )\n+\n+            content = compress(page.content.encode('utf-8'))\n+\n             return {\n                 'head': page.head,\n-                'content': page.content,\n+                'content': content,\n                 'last_modified': page.last_modified,\n                 'expires': page.expires if page.is_loaded else 0,\n             }\n@@ -41,7 +58,8 @@\n         return self.data['head']\n \n     def content(self):\n-        return self.data['content']\n+        content = decompress(self.data['content']).decode('utf-8')\n+        return content\n \n \n def invalidate_instance(instance):\n", "issue": "Compress HTML pages in the cache\nExercise and chapter pages are stored in the cache and only update if the backend reports that there is a change. Some pages might be large (e.g. 1M), but do compress relatively well. Memcached API supports compression on the fly, but that is not usable over django API.\r\n\r\nThus, we should at least compress HTML content manually. Alternatively, we can specialize `CachedAbstract` for memcached, which would also allow us to use `cas` operation.\r\n\r\nRelevant files:\r\n* `lib/cache/cached.py`\r\n* `exercise/cache/exercise.py` (`content` in `_generate_data(...)` and `content()`)\n", "before_files": [{"content": "import time\nfrom django.conf import settings\nfrom django.db.models.signals import post_save, post_delete\n\nfrom lib.cache import CachedAbstract\nfrom lib.remote_page import RemotePageNotModified\nfrom ..protocol.aplus import load_exercise_page\n\n\nclass ExerciseCache(CachedAbstract):\n    \"\"\" Exercise HTML content \"\"\"\n    KEY_PREFIX = \"exercise\"\n\n    def __init__(self, exercise, language, request, students, url_name):\n        self.exercise = exercise\n        self.load_args = [language, request, students, url_name]\n        super().__init__(exercise, modifiers=[language])\n\n    def _needs_generation(self, data):\n        expires = data['expires'] if data else None\n        return not expires or time.time() > expires\n\n    def _generate_data(self, exercise, data=None):\n        try:\n            page = exercise.load_page(\n                *self.load_args,\n                last_modified=data['last_modified'] if data else None\n            )\n            return {\n                'head': page.head,\n                'content': page.content,\n                'last_modified': page.last_modified,\n                'expires': page.expires if page.is_loaded else 0,\n            }\n        except RemotePageNotModified as e:\n            if e.expires:\n                data['expires'] = e.expires\n            return data\n\n    def head(self):\n        return self.data['head']\n\n    def content(self):\n        return self.data['content']\n\n\ndef invalidate_instance(instance):\n    for module in instance.course_modules.all():\n        for exercise in module.learning_objects.all():\n            for language,_ in settings.LANGUAGES:\n                ExerciseCache.invalidate(exercise, modifiers=[language])\n", "path": "exercise/cache/exercise.py"}]}