We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Fix ASRSnippetBundle.empty calculation to work with Jobs


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `snippets/base/bundles.py`
Content:
```
1 import hashlib
2 import json
3 from datetime import datetime
4 from urllib.parse import urljoin, urlparse
5 
6 from django.apps import apps
7 from django.conf import settings
8 from django.core.cache import cache
9 from django.core.files.base import ContentFile
10 from django.core.files.storage import default_storage
11 from django.template.loader import render_to_string
12 from django.utils.functional import cached_property
13 
14 import brotli
15 
16 from snippets.base import util
17 from snippets.base import models
18 
19 
20 ONE_DAY = 60 * 60 * 24
21 
22 SNIPPET_FETCH_TEMPLATE_HASH = hashlib.sha1(
23     render_to_string(
24         'base/fetch_snippets.jinja',
25         {
26             'date': '',
27             'snippet_ids': [],
28             'snippets_json': '',
29             'locale': 'xx',
30             'settings': settings,
31             'current_firefox_major_version': '00',
32             'metrics_url': settings.METRICS_URL,
33         }
34     ).encode('utf-8')).hexdigest()
35 
36 SNIPPET_FETCH_TEMPLATE_AS_HASH = hashlib.sha1(
37     render_to_string(
38         'base/fetch_snippets_as.jinja',
39         {
40             'date': '',
41             'snippet_ids': [],
42             'snippets_json': '',
43             'locale': 'xx',
44             'settings': settings,
45             'current_firefox_major_version': '00',
46             'metrics_url': settings.METRICS_URL,
47         }
48     ).encode('utf-8')).hexdigest()
49 
50 # On application load combine all the version strings of all available
51 # templates into one. To be used in ASRSnippetBundle.key method to calculate
52 # the bundle key. The point is that this string should change when the Template
53 # schema changes.
54 TEMPLATES_NG_VERSIONS = '-'.join([
55     model.VERSION
56     for model in apps.get_models()
57     if issubclass(model, models.Template) and not model.__name__ == 'Template'
58 ])
59 
60 
61 class SnippetBundle(object):
62     """
63     Group of snippets to be sent to a particular client configuration.
64     """
65     def __init__(self, client):
66         self.client = client
67 
68     @cached_property
69     def key(self):
70         """A unique key for this bundle as a sha1 hexdigest."""
71         # Key should consist of snippets that are in the bundle. This part
72         # accounts for all the properties sent by the Client, since the
73         # self.snippets lists snippets are all filters and CMRs have been
74         # applied.
75         key_properties = [
76             '{id}-{date}-{templatedate}'.format(id=snippet.id,
77                                                 date=snippet.modified.isoformat(),
78                                                 templatedate=snippet.template.modified.isoformat())
79             for snippet in self.snippets]
80 
81         # Additional values used to calculate the key are the templates and the
82         # variables used to render them besides snippets.
83         key_properties.extend([
84             str(self.client.startpage_version),
85             self.client.locale,
86             util.current_firefox_major_version(),
87             str(settings.BUNDLE_BROTLI_COMPRESS),
88         ])
89         if self.client.startpage_version >= 5:
90             key_properties.append(SNIPPET_FETCH_TEMPLATE_AS_HASH)
91         else:
92             key_properties.append(SNIPPET_FETCH_TEMPLATE_HASH)
93 
94         key_string = '_'.join(key_properties)
95         return hashlib.sha1(key_string.encode('utf-8')).hexdigest()
96 
97     @property
98     def empty(self):
99         return len(self.snippets) == 0
100 
101     @property
102     def cache_key(self):
103         return 'bundle_' + self.key
104 
105     @property
106     def cached(self):
107         if cache.get(self.cache_key):
108             return True
109 
110         # Check if available on S3 already.
111         if default_storage.exists(self.filename):
112             cache.set(self.cache_key, True, ONE_DAY)
113             return True
114 
115         return False
116 
117     @property
118     def expired(self):
119         """
120         If True, the code for this bundle should be re-generated before
121         use.
122         """
123         return not cache.get(self.cache_key)
124 
125     @property
126     def filename(self):
127         return urljoin(settings.MEDIA_BUNDLES_ROOT, 'bundle_{0}.html'.format(self.key))
128 
129     @property
130     def url(self):
131         bundle_url = default_storage.url(self.filename)
132         full_url = urljoin(settings.SITE_URL, bundle_url).split('?')[0]
133         cdn_url = getattr(settings, 'CDN_URL', None)
134         if cdn_url:
135             full_url = urljoin(cdn_url, urlparse(bundle_url).path)
136 
137         return full_url
138 
139     @cached_property
140     def snippets(self):
141         return (models.Snippet.objects
142                 .filter(published=True)
143                 .match_client(self.client)
144                 .select_related('template')
145                 .prefetch_related('countries', 'exclude_from_search_providers')
146                 .filter_by_available())
147 
148     def generate(self):
149         """Generate and save the code for this snippet bundle."""
150         template = 'base/fetch_snippets.jinja'
151         if self.client.startpage_version == 5:
152             template = 'base/fetch_snippets_as.jinja'
153         bundle_content = render_to_string(template, {
154             'snippet_ids': [snippet.id for snippet in self.snippets],
155             'snippets_json': json.dumps([s.to_dict() for s in self.snippets]),
156             'client': self.client,
157             'locale': self.client.locale,
158             'settings': settings,
159             'current_firefox_major_version': util.current_firefox_major_version(),
160         })
161 
162         if isinstance(bundle_content, str):
163             bundle_content = bundle_content.encode('utf-8')
164 
165         if (settings.BUNDLE_BROTLI_COMPRESS and self.client.startpage_version >= 5):
166             content_file = ContentFile(brotli.compress(bundle_content))
167             content_file.content_encoding = 'br'
168         else:
169             content_file = ContentFile(bundle_content)
170 
171         default_storage.save(self.filename, content_file)
172         cache.set(self.cache_key, True, ONE_DAY)
173 
174 
175 class ASRSnippetBundle(SnippetBundle):
176 
177     @cached_property
178     def key(self):
179         """A unique key for this bundle as a sha1 hexdigest."""
180         # Key should consist of snippets that are in the bundle. This part
181         # accounts for all the properties sent by the Client, since the
182         # self.snippets lists snippets are all filters and CMRs have been
183         # applied.
184         #
185         # Key must change when Snippet or related Template, Campaign or Target
186         # get updated.
187         key_properties = []
188         for job in self.jobs:
189             attributes = [
190                 job.id,
191                 job.snippet.modified.isoformat(),
192             ]
193 
194             key_properties.append('-'.join([str(x) for x in attributes]))
195 
196         # Additional values used to calculate the key are the templates and the
197         # variables used to render them besides snippets.
198         key_properties.extend([
199             str(self.client.startpage_version),
200             self.client.locale,
201             str(settings.BUNDLE_BROTLI_COMPRESS),
202             TEMPLATES_NG_VERSIONS,
203         ])
204 
205         key_string = '_'.join(key_properties)
206         return hashlib.sha1(key_string.encode('utf-8')).hexdigest()
207 
208     @property
209     def filename(self):
210         return urljoin(settings.MEDIA_BUNDLES_ROOT, 'bundle_{0}.json'.format(self.key))
211 
212     @cached_property
213     def jobs(self):
214         return (models.Job.objects.filter(status=models.Job.PUBLISHED)
215                 .select_related('snippet')
216                 .match_client(self.client))
217 
218     def generate(self):
219         """Generate and save the code for this snippet bundle."""
220         # Generate the new AS Router bundle format
221         data = [job.render() for job in self.jobs]
222         bundle_content = json.dumps({
223             'messages': data,
224             'metadata': {
225                 'generated_at': datetime.utcnow().isoformat(),
226                 'number_of_snippets': len(data),
227             }
228         })
229 
230         if isinstance(bundle_content, str):
231             bundle_content = bundle_content.encode('utf-8')
232 
233         if settings.BUNDLE_BROTLI_COMPRESS:
234             content_file = ContentFile(brotli.compress(bundle_content))
235             content_file.content_encoding = 'br'
236         else:
237             content_file = ContentFile(bundle_content)
238 
239         default_storage.save(self.filename, content_file)
240         cache.set(self.cache_key, True, ONE_DAY)
241
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/snippets/base/bundles.py b/snippets/base/bundles.py
--- a/snippets/base/bundles.py
+++ b/snippets/base/bundles.py
@@ -205,6 +205,10 @@
         key_string = '_'.join(key_properties)
         return hashlib.sha1(key_string.encode('utf-8')).hexdigest()
 
+    @property
+    def empty(self):
+        return len(self.jobs) == 0
+
     @property
     def filename(self):
         return urljoin(settings.MEDIA_BUNDLES_ROOT, 'bundle_{0}.json'.format(self.key))

{"golden_diff": "diff --git a/snippets/base/bundles.py b/snippets/base/bundles.py\n--- a/snippets/base/bundles.py\n+++ b/snippets/base/bundles.py\n@@ -205,6 +205,10 @@\n         key_string = '_'.join(key_properties)\n         return hashlib.sha1(key_string.encode('utf-8')).hexdigest()\n \n+    @property\n+    def empty(self):\n+        return len(self.jobs) == 0\n+\n     @property\n     def filename(self):\n         return urljoin(settings.MEDIA_BUNDLES_ROOT, 'bundle_{0}.json'.format(self.key))\n", "issue": "Fix ASRSnippetBundle.empty calculation to work with Jobs\n\n", "before_files": [{"content": "import hashlib\nimport json\nfrom datetime import datetime\nfrom urllib.parse import urljoin, urlparse\n\nfrom django.apps import apps\nfrom django.conf import settings\nfrom django.core.cache import cache\nfrom django.core.files.base import ContentFile\nfrom django.core.files.storage import default_storage\nfrom django.template.loader import render_to_string\nfrom django.utils.functional import cached_property\n\nimport brotli\n\nfrom snippets.base import util\nfrom snippets.base import models\n\n\nONE_DAY = 60 * 60 * 24\n\nSNIPPET_FETCH_TEMPLATE_HASH = hashlib.sha1(\n    render_to_string(\n        'base/fetch_snippets.jinja',\n        {\n            'date': '',\n            'snippet_ids': [],\n            'snippets_json': '',\n            'locale': 'xx',\n            'settings': settings,\n            'current_firefox_major_version': '00',\n            'metrics_url': settings.METRICS_URL,\n        }\n    ).encode('utf-8')).hexdigest()\n\nSNIPPET_FETCH_TEMPLATE_AS_HASH = hashlib.sha1(\n    render_to_string(\n        'base/fetch_snippets_as.jinja',\n        {\n            'date': '',\n            'snippet_ids': [],\n            'snippets_json': '',\n            'locale': 'xx',\n            'settings': settings,\n            'current_firefox_major_version': '00',\n            'metrics_url': settings.METRICS_URL,\n        }\n    ).encode('utf-8')).hexdigest()\n\n# On application load combine all the version strings of all available\n# templates into one. To be used in ASRSnippetBundle.key method to calculate\n# the bundle key. The point is that this string should change when the Template\n# schema changes.\nTEMPLATES_NG_VERSIONS = '-'.join([\n    model.VERSION\n    for model in apps.get_models()\n    if issubclass(model, models.Template) and not model.__name__ == 'Template'\n])\n\n\nclass SnippetBundle(object):\n    \"\"\"\n    Group of snippets to be sent to a particular client configuration.\n    \"\"\"\n    def __init__(self, client):\n        self.client = client\n\n    @cached_property\n    def key(self):\n        \"\"\"A unique key for this bundle as a sha1 hexdigest.\"\"\"\n        # Key should consist of snippets that are in the bundle. This part\n        # accounts for all the properties sent by the Client, since the\n        # self.snippets lists snippets are all filters and CMRs have been\n        # applied.\n        key_properties = [\n            '{id}-{date}-{templatedate}'.format(id=snippet.id,\n                                                date=snippet.modified.isoformat(),\n                                                templatedate=snippet.template.modified.isoformat())\n            for snippet in self.snippets]\n\n        # Additional values used to calculate the key are the templates and the\n        # variables used to render them besides snippets.\n        key_properties.extend([\n            str(self.client.startpage_version),\n            self.client.locale,\n            util.current_firefox_major_version(),\n            str(settings.BUNDLE_BROTLI_COMPRESS),\n        ])\n        if self.client.startpage_version >= 5:\n            key_properties.append(SNIPPET_FETCH_TEMPLATE_AS_HASH)\n        else:\n            key_properties.append(SNIPPET_FETCH_TEMPLATE_HASH)\n\n        key_string = '_'.join(key_properties)\n        return hashlib.sha1(key_string.encode('utf-8')).hexdigest()\n\n    @property\n    def empty(self):\n        return len(self.snippets) == 0\n\n    @property\n    def cache_key(self):\n        return 'bundle_' + self.key\n\n    @property\n    def cached(self):\n        if cache.get(self.cache_key):\n            return True\n\n        # Check if available on S3 already.\n        if default_storage.exists(self.filename):\n            cache.set(self.cache_key, True, ONE_DAY)\n            return True\n\n        return False\n\n    @property\n    def expired(self):\n        \"\"\"\n        If True, the code for this bundle should be re-generated before\n        use.\n        \"\"\"\n        return not cache.get(self.cache_key)\n\n    @property\n    def filename(self):\n        return urljoin(settings.MEDIA_BUNDLES_ROOT, 'bundle_{0}.html'.format(self.key))\n\n    @property\n    def url(self):\n        bundle_url = default_storage.url(self.filename)\n        full_url = urljoin(settings.SITE_URL, bundle_url).split('?')[0]\n        cdn_url = getattr(settings, 'CDN_URL', None)\n        if cdn_url:\n            full_url = urljoin(cdn_url, urlparse(bundle_url).path)\n\n        return full_url\n\n    @cached_property\n    def snippets(self):\n        return (models.Snippet.objects\n                .filter(published=True)\n                .match_client(self.client)\n                .select_related('template')\n                .prefetch_related('countries', 'exclude_from_search_providers')\n                .filter_by_available())\n\n    def generate(self):\n        \"\"\"Generate and save the code for this snippet bundle.\"\"\"\n        template = 'base/fetch_snippets.jinja'\n        if self.client.startpage_version == 5:\n            template = 'base/fetch_snippets_as.jinja'\n        bundle_content = render_to_string(template, {\n            'snippet_ids': [snippet.id for snippet in self.snippets],\n            'snippets_json': json.dumps([s.to_dict() for s in self.snippets]),\n            'client': self.client,\n            'locale': self.client.locale,\n            'settings': settings,\n            'current_firefox_major_version': util.current_firefox_major_version(),\n        })\n\n        if isinstance(bundle_content, str):\n            bundle_content = bundle_content.encode('utf-8')\n\n        if (settings.BUNDLE_BROTLI_COMPRESS and self.client.startpage_version >= 5):\n            content_file = ContentFile(brotli.compress(bundle_content))\n            content_file.content_encoding = 'br'\n        else:\n            content_file = ContentFile(bundle_content)\n\n        default_storage.save(self.filename, content_file)\n        cache.set(self.cache_key, True, ONE_DAY)\n\n\nclass ASRSnippetBundle(SnippetBundle):\n\n    @cached_property\n    def key(self):\n        \"\"\"A unique key for this bundle as a sha1 hexdigest.\"\"\"\n        # Key should consist of snippets that are in the bundle. This part\n        # accounts for all the properties sent by the Client, since the\n        # self.snippets lists snippets are all filters and CMRs have been\n        # applied.\n        #\n        # Key must change when Snippet or related Template, Campaign or Target\n        # get updated.\n        key_properties = []\n        for job in self.jobs:\n            attributes = [\n                job.id,\n                job.snippet.modified.isoformat(),\n            ]\n\n            key_properties.append('-'.join([str(x) for x in attributes]))\n\n        # Additional values used to calculate the key are the templates and the\n        # variables used to render them besides snippets.\n        key_properties.extend([\n            str(self.client.startpage_version),\n            self.client.locale,\n            str(settings.BUNDLE_BROTLI_COMPRESS),\n            TEMPLATES_NG_VERSIONS,\n        ])\n\n        key_string = '_'.join(key_properties)\n        return hashlib.sha1(key_string.encode('utf-8')).hexdigest()\n\n    @property\n    def filename(self):\n        return urljoin(settings.MEDIA_BUNDLES_ROOT, 'bundle_{0}.json'.format(self.key))\n\n    @cached_property\n    def jobs(self):\n        return (models.Job.objects.filter(status=models.Job.PUBLISHED)\n                .select_related('snippet')\n                .match_client(self.client))\n\n    def generate(self):\n        \"\"\"Generate and save the code for this snippet bundle.\"\"\"\n        # Generate the new AS Router bundle format\n        data = [job.render() for job in self.jobs]\n        bundle_content = json.dumps({\n            'messages': data,\n            'metadata': {\n                'generated_at': datetime.utcnow().isoformat(),\n                'number_of_snippets': len(data),\n            }\n        })\n\n        if isinstance(bundle_content, str):\n            bundle_content = bundle_content.encode('utf-8')\n\n        if settings.BUNDLE_BROTLI_COMPRESS:\n            content_file = ContentFile(brotli.compress(bundle_content))\n            content_file.content_encoding = 'br'\n        else:\n            content_file = ContentFile(bundle_content)\n\n        default_storage.save(self.filename, content_file)\n        cache.set(self.cache_key, True, ONE_DAY)\n", "path": "snippets/base/bundles.py"}], "after_files": [{"content": "import hashlib\nimport json\nfrom datetime import datetime\nfrom urllib.parse import urljoin, urlparse\n\nfrom django.apps import apps\nfrom django.conf import settings\nfrom django.core.cache import cache\nfrom django.core.files.base import ContentFile\nfrom django.core.files.storage import default_storage\nfrom django.template.loader import render_to_string\nfrom django.utils.functional import cached_property\n\nimport brotli\n\nfrom snippets.base import util\nfrom snippets.base import models\n\n\nONE_DAY = 60 * 60 * 24\n\nSNIPPET_FETCH_TEMPLATE_HASH = hashlib.sha1(\n    render_to_string(\n        'base/fetch_snippets.jinja',\n        {\n            'date': '',\n            'snippet_ids': [],\n            'snippets_json': '',\n            'locale': 'xx',\n            'settings': settings,\n            'current_firefox_major_version': '00',\n            'metrics_url': settings.METRICS_URL,\n        }\n    ).encode('utf-8')).hexdigest()\n\nSNIPPET_FETCH_TEMPLATE_AS_HASH = hashlib.sha1(\n    render_to_string(\n        'base/fetch_snippets_as.jinja',\n        {\n            'date': '',\n            'snippet_ids': [],\n            'snippets_json': '',\n            'locale': 'xx',\n            'settings': settings,\n            'current_firefox_major_version': '00',\n            'metrics_url': settings.METRICS_URL,\n        }\n    ).encode('utf-8')).hexdigest()\n\n# On application load combine all the version strings of all available\n# templates into one. To be used in ASRSnippetBundle.key method to calculate\n# the bundle key. The point is that this string should change when the Template\n# schema changes.\nTEMPLATES_NG_VERSIONS = '-'.join([\n    model.VERSION\n    for model in apps.get_models()\n    if issubclass(model, models.Template) and not model.__name__ == 'Template'\n])\n\n\nclass SnippetBundle(object):\n    \"\"\"\n    Group of snippets to be sent to a particular client configuration.\n    \"\"\"\n    def __init__(self, client):\n        self.client = client\n\n    @cached_property\n    def key(self):\n        \"\"\"A unique key for this bundle as a sha1 hexdigest.\"\"\"\n        # Key should consist of snippets that are in the bundle. This part\n        # accounts for all the properties sent by the Client, since the\n        # self.snippets lists snippets are all filters and CMRs have been\n        # applied.\n        key_properties = [\n            '{id}-{date}-{templatedate}'.format(id=snippet.id,\n                                                date=snippet.modified.isoformat(),\n                                                templatedate=snippet.template.modified.isoformat())\n            for snippet in self.snippets]\n\n        # Additional values used to calculate the key are the templates and the\n        # variables used to render them besides snippets.\n        key_properties.extend([\n            str(self.client.startpage_version),\n            self.client.locale,\n            util.current_firefox_major_version(),\n            str(settings.BUNDLE_BROTLI_COMPRESS),\n        ])\n        if self.client.startpage_version >= 5:\n            key_properties.append(SNIPPET_FETCH_TEMPLATE_AS_HASH)\n        else:\n            key_properties.append(SNIPPET_FETCH_TEMPLATE_HASH)\n\n        key_string = '_'.join(key_properties)\n        return hashlib.sha1(key_string.encode('utf-8')).hexdigest()\n\n    @property\n    def empty(self):\n        return len(self.snippets) == 0\n\n    @property\n    def cache_key(self):\n        return 'bundle_' + self.key\n\n    @property\n    def cached(self):\n        if cache.get(self.cache_key):\n            return True\n\n        # Check if available on S3 already.\n        if default_storage.exists(self.filename):\n            cache.set(self.cache_key, True, ONE_DAY)\n            return True\n\n        return False\n\n    @property\n    def expired(self):\n        \"\"\"\n        If True, the code for this bundle should be re-generated before\n        use.\n        \"\"\"\n        return not cache.get(self.cache_key)\n\n    @property\n    def filename(self):\n        return urljoin(settings.MEDIA_BUNDLES_ROOT, 'bundle_{0}.html'.format(self.key))\n\n    @property\n    def url(self):\n        bundle_url = default_storage.url(self.filename)\n        full_url = urljoin(settings.SITE_URL, bundle_url).split('?')[0]\n        cdn_url = getattr(settings, 'CDN_URL', None)\n        if cdn_url:\n            full_url = urljoin(cdn_url, urlparse(bundle_url).path)\n\n        return full_url\n\n    @cached_property\n    def snippets(self):\n        return (models.Snippet.objects\n                .filter(published=True)\n                .match_client(self.client)\n                .select_related('template')\n                .prefetch_related('countries', 'exclude_from_search_providers')\n                .filter_by_available())\n\n    def generate(self):\n        \"\"\"Generate and save the code for this snippet bundle.\"\"\"\n        template = 'base/fetch_snippets.jinja'\n        if self.client.startpage_version == 5:\n            template = 'base/fetch_snippets_as.jinja'\n        bundle_content = render_to_string(template, {\n            'snippet_ids': [snippet.id for snippet in self.snippets],\n            'snippets_json': json.dumps([s.to_dict() for s in self.snippets]),\n            'client': self.client,\n            'locale': self.client.locale,\n            'settings': settings,\n            'current_firefox_major_version': util.current_firefox_major_version(),\n        })\n\n        if isinstance(bundle_content, str):\n            bundle_content = bundle_content.encode('utf-8')\n\n        if (settings.BUNDLE_BROTLI_COMPRESS and self.client.startpage_version >= 5):\n            content_file = ContentFile(brotli.compress(bundle_content))\n            content_file.content_encoding = 'br'\n        else:\n            content_file = ContentFile(bundle_content)\n\n        default_storage.save(self.filename, content_file)\n        cache.set(self.cache_key, True, ONE_DAY)\n\n\nclass ASRSnippetBundle(SnippetBundle):\n\n    @cached_property\n    def key(self):\n        \"\"\"A unique key for this bundle as a sha1 hexdigest.\"\"\"\n        # Key should consist of snippets that are in the bundle. This part\n        # accounts for all the properties sent by the Client, since the\n        # self.snippets lists snippets are all filters and CMRs have been\n        # applied.\n        #\n        # Key must change when Snippet or related Template, Campaign or Target\n        # get updated.\n        key_properties = []\n        for job in self.jobs:\n            attributes = [\n                job.id,\n                job.snippet.modified.isoformat(),\n            ]\n\n            key_properties.append('-'.join([str(x) for x in attributes]))\n\n        # Additional values used to calculate the key are the templates and the\n        # variables used to render them besides snippets.\n        key_properties.extend([\n            str(self.client.startpage_version),\n            self.client.locale,\n            str(settings.BUNDLE_BROTLI_COMPRESS),\n            TEMPLATES_NG_VERSIONS,\n        ])\n\n        key_string = '_'.join(key_properties)\n        return hashlib.sha1(key_string.encode('utf-8')).hexdigest()\n\n    @property\n    def empty(self):\n        return len(self.jobs) == 0\n\n    @property\n    def filename(self):\n        return urljoin(settings.MEDIA_BUNDLES_ROOT, 'bundle_{0}.json'.format(self.key))\n\n    @cached_property\n    def jobs(self):\n        return (models.Job.objects.filter(status=models.Job.PUBLISHED)\n                .select_related('snippet')\n                .match_client(self.client))\n\n    def generate(self):\n        \"\"\"Generate and save the code for this snippet bundle.\"\"\"\n        # Generate the new AS Router bundle format\n        data = [job.render() for job in self.jobs]\n        bundle_content = json.dumps({\n            'messages': data,\n            'metadata': {\n                'generated_at': datetime.utcnow().isoformat(),\n                'number_of_snippets': len(data),\n            }\n        })\n\n        if isinstance(bundle_content, str):\n            bundle_content = bundle_content.encode('utf-8')\n\n        if settings.BUNDLE_BROTLI_COMPRESS:\n            content_file = ContentFile(brotli.compress(bundle_content))\n            content_file.content_encoding = 'br'\n        else:\n            content_file = ContentFile(bundle_content)\n\n        default_storage.save(self.filename, content_file)\n        cache.set(self.cache_key, True, ONE_DAY)\n", "path": "snippets/base/bundles.py"}]}