We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Summary download not working
### Describe the bug
Some summaries (for example Data Mining 2019-2020 Practice Exam Midterms) are not downloaded when clicked. They also do not work when trying to view them through the Site Administration. Pressing these buttons only leads to a new empty tab. 

Some summaries still work however.

### How to reproduce
Steps to reproduce the behaviour:
1. Go to 'education'
2. Scroll down to 'data mining'
3. Click on 'data mining'
4. Click on '2019-2020'
5. Click on 'Practice Exam Midterm'

### Expected behaviour
Clicking this button should download the associated file.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `website/education/views.py`
Content:
```
1 """Views provided by the education package."""
2 import os
3 from datetime import date, datetime
4 
5 from django.contrib.auth.decorators import login_required
6 from django.contrib.messages.views import SuccessMessageMixin
7 from django.core.exceptions import PermissionDenied
8 from django.db.models import Count
9 from django.http import HttpResponse
10 from django.shortcuts import redirect
11 from django.urls import reverse_lazy
12 from django.utils import timezone
13 from django.utils.decorators import method_decorator
14 from django.utils.translation import gettext_lazy as _
15 from django.views.generic import CreateView, DetailView, ListView, TemplateView
16 
17 from members.decorators import membership_required
18 from utils.media.services import get_media_url
19 
20 from . import emails
21 from .forms import AddExamForm, AddSummaryForm
22 from .models import Category, Course, Exam, Summary
23 
24 
25 class CourseIndexView(ListView):
26     """Render an overview of the courses."""
27 
28     queryset = (
29         Course.objects.filter(until=None)
30         .prefetch_related("categories", "old_courses")
31         .annotate(summary_count=Count("summary"))
32         .annotate(exam_count=Count("exam"))
33     )
34     template_name = "education/courses.html"
35 
36     def get_ordering(self) -> str:
37         return "name"
38 
39     def get_context_data(self, **kwargs) -> dict:
40         context = super().get_context_data(**kwargs)
41         context.update(
42             {
43                 "courses": (
44                     {
45                         "course_code": x.course_code,
46                         "name": x.name,
47                         "categories": x.categories.all(),
48                         "document_count": sum(
49                             [
50                                 x.summary_count,
51                                 x.exam_count,
52                             ]
53                             + [
54                                 c.summary_set.filter(accepted=True).count()
55                                 + c.exam_set.filter(accepted=True).count()
56                                 for c in x.old_courses.all()
57                             ]
58                         ),
59                         "url": x.get_absolute_url(),
60                     }
61                     for x in context["object_list"]
62                 ),
63                 "categories": Category.objects.all(),
64             }
65         )
66         return context
67 
68 
69 class CourseDetailView(DetailView):
70     """Render the detail page of one specific course."""
71 
72     model = Course
73     context_object_name = "course"
74     template_name = "education/course.html"
75 
76     def get_context_data(self, **kwargs) -> dict:
77         context = super().get_context_data(**kwargs)
78         obj = context["course"]
79         courses = list(obj.old_courses.all())
80         courses.append(obj)
81         items = {}
82         for course in courses:
83             for summary in course.summary_set.filter(accepted=True):
84                 if summary.year not in items:
85                     items[summary.year] = {
86                         "summaries": [],
87                         "exams": [],
88                         "legacy": course if course.pk != obj.pk else None,
89                     }
90                 items[summary.year]["summaries"].append(
91                     {
92                         "year": summary.year,
93                         "name": summary.name,
94                         "language": summary.language,
95                         "id": summary.id,
96                     }
97                 )
98             for exam in course.exam_set.filter(accepted=True):
99                 if exam.year not in items:
100                     items[exam.year] = {
101                         "summaries": [],
102                         "exams": [],
103                         "legacy": course if course.pk != obj.pk else None,
104                     }
105                 items[exam.year]["exams"].append(
106                     {
107                         "type": "exam",
108                         "year": exam.year,
109                         "name": f"{exam.get_type_display()} {exam.name}",
110                         "language": exam.language,
111                         "id": exam.id,
112                     }
113                 )
114         context.update({"items": sorted(items.items(), key=lambda x: x[0])})
115         return context
116 
117 
118 @method_decorator(login_required, "dispatch")
119 @method_decorator(membership_required, "dispatch")
120 class ExamDetailView(DetailView):
121     """Fetch and output the specified exam."""
122 
123     model = Exam
124 
125     def get(self, request, *args, **kwargs) -> HttpResponse:
126         response = super().get(request, *args, **kwargs)
127         exam = response.context_data["object"]
128         exam.download_count += 1
129         exam.save()
130 
131         ext = os.path.splitext(exam.file.name)[1]
132         filename = f"{exam.course.name}-exam{exam.year}{ext}"
133         return redirect(get_media_url(exam.file, filename))
134 
135 
136 @method_decorator(login_required, "dispatch")
137 @method_decorator(membership_required, "dispatch")
138 class SummaryDetailView(DetailView):
139     """Fetch and output the specified summary."""
140 
141     model = Summary
142 
143     def get(self, request, *args, **kwargs) -> HttpResponse:
144         response = super().get(request, *args, **kwargs)
145         obj = response.context_data["object"]
146         obj.download_count += 1
147         obj.save()
148 
149         ext = os.path.splitext(obj.file.name)[1]
150         filename = f"{obj.course.name}-summary{obj.year}{ext}"
151         return redirect(get_media_url(obj.file, filename))
152 
153 
154 @method_decorator(login_required, "dispatch")
155 @method_decorator(membership_required, "dispatch")
156 class ExamCreateView(SuccessMessageMixin, CreateView):
157     """Render the form to submit a new exam."""
158 
159     model = Exam
160     form_class = AddExamForm
161     template_name = "education/add_exam.html"
162     success_url = reverse_lazy("education:submit-exam")
163     success_message = _("Exam submitted successfully.")
164 
165     def get_initial(self) -> dict:
166         initial = super().get_initial()
167         initial["exam_date"] = date.today()
168         initial["course"] = self.kwargs.get("pk", None)
169         return initial
170 
171     def form_valid(self, form) -> HttpResponse:
172         self.object = form.save(commit=False)
173         self.object.uploader = self.request.member
174         self.object.uploader_date = datetime.now()
175         self.object.save()
176         emails.send_document_notification(self.object)
177         return super().form_valid(form)
178 
179 
180 @method_decorator(login_required, "dispatch")
181 @method_decorator(membership_required, "dispatch")
182 class SummaryCreateView(SuccessMessageMixin, CreateView):
183     """Render the form to submit a new summary."""
184 
185     model = Summary
186     form_class = AddSummaryForm
187     template_name = "education/add_summary.html"
188     success_url = reverse_lazy("education:submit-summary")
189     success_message = _("Summary submitted successfully.")
190 
191     def get_initial(self):
192         initial = super().get_initial()
193         initial["author"] = self.request.member.get_full_name()
194         initial["course"] = self.kwargs.get("pk", None)
195         return initial
196 
197     def form_valid(self, form) -> HttpResponse:
198         self.object = form.save(commit=False)
199         self.object.uploader = self.request.member
200         self.object.uploader_date = datetime.now()
201         self.object.save()
202         emails.send_document_notification(self.object)
203         return super().form_valid(form)
204 
205 
206 @method_decorator(login_required, "dispatch")
207 class BookInfoView(TemplateView):
208     """Render a page with information about book sale.
209 
210     Only available to members and to-be members
211     """
212 
213     template_name = "education/books.html"
214 
215     def dispatch(self, request, *args, **kwargs) -> HttpResponse:
216         if request.member.has_active_membership() or (
217             request.member.earliest_membership
218             and request.member.earliest_membership.since > timezone.now().date()
219         ):
220             return super().dispatch(request, *args, **kwargs)
221         raise PermissionDenied
222
```

Path: `website/utils/media/services.py`
Content:
```
1 import io
2 import os
3 
4 from django.conf import settings
5 from django.core import signing
6 from django.core.files.base import ContentFile
7 from django.core.files.storage import DefaultStorage, get_storage_class
8 from django.core.files.uploadedfile import InMemoryUploadedFile
9 from django.db.models.fields.files import FieldFile, ImageFieldFile
10 from django.urls import reverse
11 
12 
13 def save_image(storage, image, path, format):
14     buffer = io.BytesIO()
15     image.convert("RGB" if format == "JPEG" else "RGBA").save(fp=buffer, format=format)
16     buff_val = buffer.getvalue()
17     content = ContentFile(buff_val)
18     file = InMemoryUploadedFile(
19         content,
20         None,
21         f"foo.{format.lower()}",
22         f"image/{format.lower()}",
23         content.tell,
24         None,
25     )
26     return storage.save(path, file)
27 
28 
29 def get_media_url(file, attachment=False):
30     """Get the url of the provided media file to serve in a browser.
31 
32     If the file is private a signature will be added.
33     Do NOT use this with user input
34     :param file: the file field
35     :param attachment: True if the file is a forced download
36     :return: the url of the media
37     """
38     storage = DefaultStorage()
39     file_name = file
40     if isinstance(file, (ImageFieldFile, FieldFile)):
41         storage = file.storage
42         file_name = file.name
43 
44     return f"{storage.url(file_name, attachment)}"
45 
46 
47 def get_thumbnail_url(file, size, fit=True):
48     """Get the thumbnail url of a media file, NEVER use this with user input.
49 
50     If the thumbnail exists this function will return the url of the
51     media file, with signature if necessary. Does it not yet exist a route
52     that executes the :func:`utils.media.views.generate_thumbnail`
53     will be the output.
54     :param file: the file field
55     :param size: size of the image
56     :param fit: False to keep the aspect ratio, True to crop
57     :return: get-thumbnail path
58     """
59     storage = DefaultStorage()
60     name = file
61 
62     if isinstance(file, (ImageFieldFile, FieldFile)):
63         storage = file.storage
64         name = file.name
65 
66     is_public = isinstance(storage, get_storage_class(settings.PUBLIC_FILE_STORAGE))
67     size_fit = f"{size}_{int(fit)}"
68 
69     if name.endswith(".svg") and is_public:
70         return storage.url(name)
71 
72     sig_info = {
73         "size": size,
74         "fit": int(fit),
75         "name": name,
76         "thumb_path": f"thumbnails/{size_fit}/{name}",
77         "serve_path": f"thumbnails/{size_fit}/{name}",
78         "storage": f"{storage.__class__.__module__}.{storage.__class__.__name__}",
79     }
80 
81     # We provide a URL instead of calling it as a function, so that using
82     # it means kicking off a new GET request. If we would need to check all files for the
83     # thumbnails inline, loading an album overview would have high latency.
84     return (
85         reverse("get-thumbnail", args=[os.path.join(size_fit, sig_info["name"])])
86         + f"?sig={signing.dumps(sig_info)}"
87     )
88
```
--- 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/website/education/views.py b/website/education/views.py
--- a/website/education/views.py
+++ b/website/education/views.py
@@ -124,13 +124,13 @@
 
     def get(self, request, *args, **kwargs) -> HttpResponse:
         response = super().get(request, *args, **kwargs)
-        exam = response.context_data["object"]
-        exam.download_count += 1
-        exam.save()
+        obj = response.context_data["object"]
+        obj.download_count += 1
+        obj.save()
 
-        ext = os.path.splitext(exam.file.name)[1]
-        filename = f"{exam.course.name}-exam{exam.year}{ext}"
-        return redirect(get_media_url(exam.file, filename))
+        ext = os.path.splitext(obj.file.name)[1]
+        filename = f"{obj.course.name}-summary{obj.year}{ext}"
+        return redirect(get_media_url(obj.file, filename))
 
 
 @method_decorator(login_required, "dispatch")
diff --git a/website/utils/media/services.py b/website/utils/media/services.py
--- a/website/utils/media/services.py
+++ b/website/utils/media/services.py
@@ -32,7 +32,7 @@
     If the file is private a signature will be added.
     Do NOT use this with user input
     :param file: the file field
-    :param attachment: True if the file is a forced download
+    :param attachment: filename to use for the attachment or False to not download as attachment
     :return: the url of the media
     """
     storage = DefaultStorage()

{"golden_diff": "diff --git a/website/education/views.py b/website/education/views.py\n--- a/website/education/views.py\n+++ b/website/education/views.py\n@@ -124,13 +124,13 @@\n \n     def get(self, request, *args, **kwargs) -> HttpResponse:\n         response = super().get(request, *args, **kwargs)\n-        exam = response.context_data[\"object\"]\n-        exam.download_count += 1\n-        exam.save()\n+        obj = response.context_data[\"object\"]\n+        obj.download_count += 1\n+        obj.save()\n \n-        ext = os.path.splitext(exam.file.name)[1]\n-        filename = f\"{exam.course.name}-exam{exam.year}{ext}\"\n-        return redirect(get_media_url(exam.file, filename))\n+        ext = os.path.splitext(obj.file.name)[1]\n+        filename = f\"{obj.course.name}-summary{obj.year}{ext}\"\n+        return redirect(get_media_url(obj.file, filename))\n \n \n @method_decorator(login_required, \"dispatch\")\ndiff --git a/website/utils/media/services.py b/website/utils/media/services.py\n--- a/website/utils/media/services.py\n+++ b/website/utils/media/services.py\n@@ -32,7 +32,7 @@\n     If the file is private a signature will be added.\n     Do NOT use this with user input\n     :param file: the file field\n-    :param attachment: True if the file is a forced download\n+    :param attachment: filename to use for the attachment or False to not download as attachment\n     :return: the url of the media\n     \"\"\"\n     storage = DefaultStorage()\n", "issue": "Summary download not working\n### Describe the bug\r\nSome summaries (for example Data Mining 2019-2020 Practice Exam Midterms) are not downloaded when clicked. They also do not work when trying to view them through the Site Administration. Pressing these buttons only leads to a new empty tab. \r\n\r\nSome summaries still work however.\r\n\r\n### How to reproduce\r\nSteps to reproduce the behaviour:\r\n1. Go to 'education'\r\n2. Scroll down to 'data mining'\r\n3. Click on 'data mining'\r\n4. Click on '2019-2020'\r\n5. Click on 'Practice Exam Midterm'\r\n\r\n### Expected behaviour\r\nClicking this button should download the associated file.\r\n\n", "before_files": [{"content": "\"\"\"Views provided by the education package.\"\"\"\nimport os\nfrom datetime import date, datetime\n\nfrom django.contrib.auth.decorators import login_required\nfrom django.contrib.messages.views import SuccessMessageMixin\nfrom django.core.exceptions import PermissionDenied\nfrom django.db.models import Count\nfrom django.http import HttpResponse\nfrom django.shortcuts import redirect\nfrom django.urls import reverse_lazy\nfrom django.utils import timezone\nfrom django.utils.decorators import method_decorator\nfrom django.utils.translation import gettext_lazy as _\nfrom django.views.generic import CreateView, DetailView, ListView, TemplateView\n\nfrom members.decorators import membership_required\nfrom utils.media.services import get_media_url\n\nfrom . import emails\nfrom .forms import AddExamForm, AddSummaryForm\nfrom .models import Category, Course, Exam, Summary\n\n\nclass CourseIndexView(ListView):\n    \"\"\"Render an overview of the courses.\"\"\"\n\n    queryset = (\n        Course.objects.filter(until=None)\n        .prefetch_related(\"categories\", \"old_courses\")\n        .annotate(summary_count=Count(\"summary\"))\n        .annotate(exam_count=Count(\"exam\"))\n    )\n    template_name = \"education/courses.html\"\n\n    def get_ordering(self) -> str:\n        return \"name\"\n\n    def get_context_data(self, **kwargs) -> dict:\n        context = super().get_context_data(**kwargs)\n        context.update(\n            {\n                \"courses\": (\n                    {\n                        \"course_code\": x.course_code,\n                        \"name\": x.name,\n                        \"categories\": x.categories.all(),\n                        \"document_count\": sum(\n                            [\n                                x.summary_count,\n                                x.exam_count,\n                            ]\n                            + [\n                                c.summary_set.filter(accepted=True).count()\n                                + c.exam_set.filter(accepted=True).count()\n                                for c in x.old_courses.all()\n                            ]\n                        ),\n                        \"url\": x.get_absolute_url(),\n                    }\n                    for x in context[\"object_list\"]\n                ),\n                \"categories\": Category.objects.all(),\n            }\n        )\n        return context\n\n\nclass CourseDetailView(DetailView):\n    \"\"\"Render the detail page of one specific course.\"\"\"\n\n    model = Course\n    context_object_name = \"course\"\n    template_name = \"education/course.html\"\n\n    def get_context_data(self, **kwargs) -> dict:\n        context = super().get_context_data(**kwargs)\n        obj = context[\"course\"]\n        courses = list(obj.old_courses.all())\n        courses.append(obj)\n        items = {}\n        for course in courses:\n            for summary in course.summary_set.filter(accepted=True):\n                if summary.year not in items:\n                    items[summary.year] = {\n                        \"summaries\": [],\n                        \"exams\": [],\n                        \"legacy\": course if course.pk != obj.pk else None,\n                    }\n                items[summary.year][\"summaries\"].append(\n                    {\n                        \"year\": summary.year,\n                        \"name\": summary.name,\n                        \"language\": summary.language,\n                        \"id\": summary.id,\n                    }\n                )\n            for exam in course.exam_set.filter(accepted=True):\n                if exam.year not in items:\n                    items[exam.year] = {\n                        \"summaries\": [],\n                        \"exams\": [],\n                        \"legacy\": course if course.pk != obj.pk else None,\n                    }\n                items[exam.year][\"exams\"].append(\n                    {\n                        \"type\": \"exam\",\n                        \"year\": exam.year,\n                        \"name\": f\"{exam.get_type_display()} {exam.name}\",\n                        \"language\": exam.language,\n                        \"id\": exam.id,\n                    }\n                )\n        context.update({\"items\": sorted(items.items(), key=lambda x: x[0])})\n        return context\n\n\n@method_decorator(login_required, \"dispatch\")\n@method_decorator(membership_required, \"dispatch\")\nclass ExamDetailView(DetailView):\n    \"\"\"Fetch and output the specified exam.\"\"\"\n\n    model = Exam\n\n    def get(self, request, *args, **kwargs) -> HttpResponse:\n        response = super().get(request, *args, **kwargs)\n        exam = response.context_data[\"object\"]\n        exam.download_count += 1\n        exam.save()\n\n        ext = os.path.splitext(exam.file.name)[1]\n        filename = f\"{exam.course.name}-exam{exam.year}{ext}\"\n        return redirect(get_media_url(exam.file, filename))\n\n\n@method_decorator(login_required, \"dispatch\")\n@method_decorator(membership_required, \"dispatch\")\nclass SummaryDetailView(DetailView):\n    \"\"\"Fetch and output the specified summary.\"\"\"\n\n    model = Summary\n\n    def get(self, request, *args, **kwargs) -> HttpResponse:\n        response = super().get(request, *args, **kwargs)\n        obj = response.context_data[\"object\"]\n        obj.download_count += 1\n        obj.save()\n\n        ext = os.path.splitext(obj.file.name)[1]\n        filename = f\"{obj.course.name}-summary{obj.year}{ext}\"\n        return redirect(get_media_url(obj.file, filename))\n\n\n@method_decorator(login_required, \"dispatch\")\n@method_decorator(membership_required, \"dispatch\")\nclass ExamCreateView(SuccessMessageMixin, CreateView):\n    \"\"\"Render the form to submit a new exam.\"\"\"\n\n    model = Exam\n    form_class = AddExamForm\n    template_name = \"education/add_exam.html\"\n    success_url = reverse_lazy(\"education:submit-exam\")\n    success_message = _(\"Exam submitted successfully.\")\n\n    def get_initial(self) -> dict:\n        initial = super().get_initial()\n        initial[\"exam_date\"] = date.today()\n        initial[\"course\"] = self.kwargs.get(\"pk\", None)\n        return initial\n\n    def form_valid(self, form) -> HttpResponse:\n        self.object = form.save(commit=False)\n        self.object.uploader = self.request.member\n        self.object.uploader_date = datetime.now()\n        self.object.save()\n        emails.send_document_notification(self.object)\n        return super().form_valid(form)\n\n\n@method_decorator(login_required, \"dispatch\")\n@method_decorator(membership_required, \"dispatch\")\nclass SummaryCreateView(SuccessMessageMixin, CreateView):\n    \"\"\"Render the form to submit a new summary.\"\"\"\n\n    model = Summary\n    form_class = AddSummaryForm\n    template_name = \"education/add_summary.html\"\n    success_url = reverse_lazy(\"education:submit-summary\")\n    success_message = _(\"Summary submitted successfully.\")\n\n    def get_initial(self):\n        initial = super().get_initial()\n        initial[\"author\"] = self.request.member.get_full_name()\n        initial[\"course\"] = self.kwargs.get(\"pk\", None)\n        return initial\n\n    def form_valid(self, form) -> HttpResponse:\n        self.object = form.save(commit=False)\n        self.object.uploader = self.request.member\n        self.object.uploader_date = datetime.now()\n        self.object.save()\n        emails.send_document_notification(self.object)\n        return super().form_valid(form)\n\n\n@method_decorator(login_required, \"dispatch\")\nclass BookInfoView(TemplateView):\n    \"\"\"Render a page with information about book sale.\n\n    Only available to members and to-be members\n    \"\"\"\n\n    template_name = \"education/books.html\"\n\n    def dispatch(self, request, *args, **kwargs) -> HttpResponse:\n        if request.member.has_active_membership() or (\n            request.member.earliest_membership\n            and request.member.earliest_membership.since > timezone.now().date()\n        ):\n            return super().dispatch(request, *args, **kwargs)\n        raise PermissionDenied\n", "path": "website/education/views.py"}, {"content": "import io\nimport os\n\nfrom django.conf import settings\nfrom django.core import signing\nfrom django.core.files.base import ContentFile\nfrom django.core.files.storage import DefaultStorage, get_storage_class\nfrom django.core.files.uploadedfile import InMemoryUploadedFile\nfrom django.db.models.fields.files import FieldFile, ImageFieldFile\nfrom django.urls import reverse\n\n\ndef save_image(storage, image, path, format):\n    buffer = io.BytesIO()\n    image.convert(\"RGB\" if format == \"JPEG\" else \"RGBA\").save(fp=buffer, format=format)\n    buff_val = buffer.getvalue()\n    content = ContentFile(buff_val)\n    file = InMemoryUploadedFile(\n        content,\n        None,\n        f\"foo.{format.lower()}\",\n        f\"image/{format.lower()}\",\n        content.tell,\n        None,\n    )\n    return storage.save(path, file)\n\n\ndef get_media_url(file, attachment=False):\n    \"\"\"Get the url of the provided media file to serve in a browser.\n\n    If the file is private a signature will be added.\n    Do NOT use this with user input\n    :param file: the file field\n    :param attachment: True if the file is a forced download\n    :return: the url of the media\n    \"\"\"\n    storage = DefaultStorage()\n    file_name = file\n    if isinstance(file, (ImageFieldFile, FieldFile)):\n        storage = file.storage\n        file_name = file.name\n\n    return f\"{storage.url(file_name, attachment)}\"\n\n\ndef get_thumbnail_url(file, size, fit=True):\n    \"\"\"Get the thumbnail url of a media file, NEVER use this with user input.\n\n    If the thumbnail exists this function will return the url of the\n    media file, with signature if necessary. Does it not yet exist a route\n    that executes the :func:`utils.media.views.generate_thumbnail`\n    will be the output.\n    :param file: the file field\n    :param size: size of the image\n    :param fit: False to keep the aspect ratio, True to crop\n    :return: get-thumbnail path\n    \"\"\"\n    storage = DefaultStorage()\n    name = file\n\n    if isinstance(file, (ImageFieldFile, FieldFile)):\n        storage = file.storage\n        name = file.name\n\n    is_public = isinstance(storage, get_storage_class(settings.PUBLIC_FILE_STORAGE))\n    size_fit = f\"{size}_{int(fit)}\"\n\n    if name.endswith(\".svg\") and is_public:\n        return storage.url(name)\n\n    sig_info = {\n        \"size\": size,\n        \"fit\": int(fit),\n        \"name\": name,\n        \"thumb_path\": f\"thumbnails/{size_fit}/{name}\",\n        \"serve_path\": f\"thumbnails/{size_fit}/{name}\",\n        \"storage\": f\"{storage.__class__.__module__}.{storage.__class__.__name__}\",\n    }\n\n    # We provide a URL instead of calling it as a function, so that using\n    # it means kicking off a new GET request. If we would need to check all files for the\n    # thumbnails inline, loading an album overview would have high latency.\n    return (\n        reverse(\"get-thumbnail\", args=[os.path.join(size_fit, sig_info[\"name\"])])\n        + f\"?sig={signing.dumps(sig_info)}\"\n    )\n", "path": "website/utils/media/services.py"}], "after_files": [{"content": "\"\"\"Views provided by the education package.\"\"\"\nimport os\nfrom datetime import date, datetime\n\nfrom django.contrib.auth.decorators import login_required\nfrom django.contrib.messages.views import SuccessMessageMixin\nfrom django.core.exceptions import PermissionDenied\nfrom django.db.models import Count\nfrom django.http import HttpResponse\nfrom django.shortcuts import redirect\nfrom django.urls import reverse_lazy\nfrom django.utils import timezone\nfrom django.utils.decorators import method_decorator\nfrom django.utils.translation import gettext_lazy as _\nfrom django.views.generic import CreateView, DetailView, ListView, TemplateView\n\nfrom members.decorators import membership_required\nfrom utils.media.services import get_media_url\n\nfrom . import emails\nfrom .forms import AddExamForm, AddSummaryForm\nfrom .models import Category, Course, Exam, Summary\n\n\nclass CourseIndexView(ListView):\n    \"\"\"Render an overview of the courses.\"\"\"\n\n    queryset = (\n        Course.objects.filter(until=None)\n        .prefetch_related(\"categories\", \"old_courses\")\n        .annotate(summary_count=Count(\"summary\"))\n        .annotate(exam_count=Count(\"exam\"))\n    )\n    template_name = \"education/courses.html\"\n\n    def get_ordering(self) -> str:\n        return \"name\"\n\n    def get_context_data(self, **kwargs) -> dict:\n        context = super().get_context_data(**kwargs)\n        context.update(\n            {\n                \"courses\": (\n                    {\n                        \"course_code\": x.course_code,\n                        \"name\": x.name,\n                        \"categories\": x.categories.all(),\n                        \"document_count\": sum(\n                            [\n                                x.summary_count,\n                                x.exam_count,\n                            ]\n                            + [\n                                c.summary_set.filter(accepted=True).count()\n                                + c.exam_set.filter(accepted=True).count()\n                                for c in x.old_courses.all()\n                            ]\n                        ),\n                        \"url\": x.get_absolute_url(),\n                    }\n                    for x in context[\"object_list\"]\n                ),\n                \"categories\": Category.objects.all(),\n            }\n        )\n        return context\n\n\nclass CourseDetailView(DetailView):\n    \"\"\"Render the detail page of one specific course.\"\"\"\n\n    model = Course\n    context_object_name = \"course\"\n    template_name = \"education/course.html\"\n\n    def get_context_data(self, **kwargs) -> dict:\n        context = super().get_context_data(**kwargs)\n        obj = context[\"course\"]\n        courses = list(obj.old_courses.all())\n        courses.append(obj)\n        items = {}\n        for course in courses:\n            for summary in course.summary_set.filter(accepted=True):\n                if summary.year not in items:\n                    items[summary.year] = {\n                        \"summaries\": [],\n                        \"exams\": [],\n                        \"legacy\": course if course.pk != obj.pk else None,\n                    }\n                items[summary.year][\"summaries\"].append(\n                    {\n                        \"year\": summary.year,\n                        \"name\": summary.name,\n                        \"language\": summary.language,\n                        \"id\": summary.id,\n                    }\n                )\n            for exam in course.exam_set.filter(accepted=True):\n                if exam.year not in items:\n                    items[exam.year] = {\n                        \"summaries\": [],\n                        \"exams\": [],\n                        \"legacy\": course if course.pk != obj.pk else None,\n                    }\n                items[exam.year][\"exams\"].append(\n                    {\n                        \"type\": \"exam\",\n                        \"year\": exam.year,\n                        \"name\": f\"{exam.get_type_display()} {exam.name}\",\n                        \"language\": exam.language,\n                        \"id\": exam.id,\n                    }\n                )\n        context.update({\"items\": sorted(items.items(), key=lambda x: x[0])})\n        return context\n\n\n@method_decorator(login_required, \"dispatch\")\n@method_decorator(membership_required, \"dispatch\")\nclass ExamDetailView(DetailView):\n    \"\"\"Fetch and output the specified exam.\"\"\"\n\n    model = Exam\n\n    def get(self, request, *args, **kwargs) -> HttpResponse:\n        response = super().get(request, *args, **kwargs)\n        obj = response.context_data[\"object\"]\n        obj.download_count += 1\n        obj.save()\n\n        ext = os.path.splitext(obj.file.name)[1]\n        filename = f\"{obj.course.name}-summary{obj.year}{ext}\"\n        return redirect(get_media_url(obj.file, filename))\n\n\n@method_decorator(login_required, \"dispatch\")\n@method_decorator(membership_required, \"dispatch\")\nclass SummaryDetailView(DetailView):\n    \"\"\"Fetch and output the specified summary.\"\"\"\n\n    model = Summary\n\n    def get(self, request, *args, **kwargs) -> HttpResponse:\n        response = super().get(request, *args, **kwargs)\n        obj = response.context_data[\"object\"]\n        obj.download_count += 1\n        obj.save()\n\n        ext = os.path.splitext(obj.file.name)[1]\n        filename = f\"{obj.course.name}-summary{obj.year}{ext}\"\n        return redirect(get_media_url(obj.file, filename))\n\n\n@method_decorator(login_required, \"dispatch\")\n@method_decorator(membership_required, \"dispatch\")\nclass ExamCreateView(SuccessMessageMixin, CreateView):\n    \"\"\"Render the form to submit a new exam.\"\"\"\n\n    model = Exam\n    form_class = AddExamForm\n    template_name = \"education/add_exam.html\"\n    success_url = reverse_lazy(\"education:submit-exam\")\n    success_message = _(\"Exam submitted successfully.\")\n\n    def get_initial(self) -> dict:\n        initial = super().get_initial()\n        initial[\"exam_date\"] = date.today()\n        initial[\"course\"] = self.kwargs.get(\"pk\", None)\n        return initial\n\n    def form_valid(self, form) -> HttpResponse:\n        self.object = form.save(commit=False)\n        self.object.uploader = self.request.member\n        self.object.uploader_date = datetime.now()\n        self.object.save()\n        emails.send_document_notification(self.object)\n        return super().form_valid(form)\n\n\n@method_decorator(login_required, \"dispatch\")\n@method_decorator(membership_required, \"dispatch\")\nclass SummaryCreateView(SuccessMessageMixin, CreateView):\n    \"\"\"Render the form to submit a new summary.\"\"\"\n\n    model = Summary\n    form_class = AddSummaryForm\n    template_name = \"education/add_summary.html\"\n    success_url = reverse_lazy(\"education:submit-summary\")\n    success_message = _(\"Summary submitted successfully.\")\n\n    def get_initial(self):\n        initial = super().get_initial()\n        initial[\"author\"] = self.request.member.get_full_name()\n        initial[\"course\"] = self.kwargs.get(\"pk\", None)\n        return initial\n\n    def form_valid(self, form) -> HttpResponse:\n        self.object = form.save(commit=False)\n        self.object.uploader = self.request.member\n        self.object.uploader_date = datetime.now()\n        self.object.save()\n        emails.send_document_notification(self.object)\n        return super().form_valid(form)\n\n\n@method_decorator(login_required, \"dispatch\")\nclass BookInfoView(TemplateView):\n    \"\"\"Render a page with information about book sale.\n\n    Only available to members and to-be members\n    \"\"\"\n\n    template_name = \"education/books.html\"\n\n    def dispatch(self, request, *args, **kwargs) -> HttpResponse:\n        if request.member.has_active_membership() or (\n            request.member.earliest_membership\n            and request.member.earliest_membership.since > timezone.now().date()\n        ):\n            return super().dispatch(request, *args, **kwargs)\n        raise PermissionDenied\n", "path": "website/education/views.py"}, {"content": "import io\nimport os\n\nfrom django.conf import settings\nfrom django.core import signing\nfrom django.core.files.base import ContentFile\nfrom django.core.files.storage import DefaultStorage, get_storage_class\nfrom django.core.files.uploadedfile import InMemoryUploadedFile\nfrom django.db.models.fields.files import FieldFile, ImageFieldFile\nfrom django.urls import reverse\n\n\ndef save_image(storage, image, path, format):\n    buffer = io.BytesIO()\n    image.convert(\"RGB\" if format == \"JPEG\" else \"RGBA\").save(fp=buffer, format=format)\n    buff_val = buffer.getvalue()\n    content = ContentFile(buff_val)\n    file = InMemoryUploadedFile(\n        content,\n        None,\n        f\"foo.{format.lower()}\",\n        f\"image/{format.lower()}\",\n        content.tell,\n        None,\n    )\n    return storage.save(path, file)\n\n\ndef get_media_url(file, attachment=False):\n    \"\"\"Get the url of the provided media file to serve in a browser.\n\n    If the file is private a signature will be added.\n    Do NOT use this with user input\n    :param file: the file field\n    :param attachment: filename to use for the attachment or False to not download as attachment\n    :return: the url of the media\n    \"\"\"\n    storage = DefaultStorage()\n    file_name = file\n    if isinstance(file, (ImageFieldFile, FieldFile)):\n        storage = file.storage\n        file_name = file.name\n\n    return f\"{storage.url(file_name, attachment)}\"\n\n\ndef get_thumbnail_url(file, size, fit=True):\n    \"\"\"Get the thumbnail url of a media file, NEVER use this with user input.\n\n    If the thumbnail exists this function will return the url of the\n    media file, with signature if necessary. Does it not yet exist a route\n    that executes the :func:`utils.media.views.generate_thumbnail`\n    will be the output.\n    :param file: the file field\n    :param size: size of the image\n    :param fit: False to keep the aspect ratio, True to crop\n    :return: get-thumbnail path\n    \"\"\"\n    storage = DefaultStorage()\n    name = file\n\n    if isinstance(file, (ImageFieldFile, FieldFile)):\n        storage = file.storage\n        name = file.name\n\n    is_public = isinstance(storage, get_storage_class(settings.PUBLIC_FILE_STORAGE))\n    size_fit = f\"{size}_{int(fit)}\"\n\n    if name.endswith(\".svg\") and is_public:\n        return storage.url(name)\n\n    sig_info = {\n        \"size\": size,\n        \"fit\": int(fit),\n        \"name\": name,\n        \"thumb_path\": f\"thumbnails/{size_fit}/{name}\",\n        \"serve_path\": f\"thumbnails/{size_fit}/{name}\",\n        \"storage\": f\"{storage.__class__.__module__}.{storage.__class__.__name__}\",\n    }\n\n    # We provide a URL instead of calling it as a function, so that using\n    # it means kicking off a new GET request. If we would need to check all files for the\n    # thumbnails inline, loading an album overview would have high latency.\n    return (\n        reverse(\"get-thumbnail\", args=[os.path.join(size_fit, sig_info[\"name\"])])\n        + f\"?sig={signing.dumps(sig_info)}\"\n    )\n", "path": "website/utils/media/services.py"}]}