We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Add a check for browser on reader study session detail
Don't use internet explorer or an old version of chrome.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `app/grandchallenge/workstations/views.py`
Content:
```
1 from datetime import timedelta
2 
3 from dal import autocomplete
4 from django.conf import settings
5 from django.contrib.auth import get_user_model
6 from django.contrib.auth.mixins import (
7     PermissionRequiredMixin,
8     UserPassesTestMixin,
9 )
10 from django.contrib.messages.views import SuccessMessageMixin
11 from django.core.exceptions import PermissionDenied
12 from django.http import HttpResponse
13 from django.shortcuts import get_object_or_404
14 from django.utils._os import safe_join
15 from django.utils.timezone import now
16 from django.views.generic import (
17     CreateView,
18     DetailView,
19     FormView,
20     ListView,
21     RedirectView,
22     UpdateView,
23 )
24 from guardian.mixins import (
25     LoginRequiredMixin,
26     PermissionListMixin,
27     PermissionRequiredMixin as ObjectPermissionRequiredMixin,
28 )
29 from rest_framework.decorators import action
30 from rest_framework.response import Response
31 from rest_framework.status import HTTP_400_BAD_REQUEST
32 from rest_framework.viewsets import ReadOnlyModelViewSet
33 from rest_framework_guardian.filters import ObjectPermissionsFilter
34 
35 from grandchallenge.core.permissions.rest_framework import (
36     DjangoObjectOnlyPermissions,
37 )
38 from grandchallenge.workstations.forms import (
39     EditorsForm,
40     UsersForm,
41     WorkstationForm,
42     WorkstationImageForm,
43 )
44 from grandchallenge.workstations.models import (
45     Session,
46     Workstation,
47     WorkstationImage,
48 )
49 from grandchallenge.workstations.serializers import SessionSerializer
50 from grandchallenge.workstations.utils import (
51     get_or_create_active_session,
52     get_workstation_image_or_404,
53 )
54 
55 
56 class SessionViewSet(ReadOnlyModelViewSet):
57     queryset = Session.objects.all()
58     serializer_class = SessionSerializer
59     permission_classes = (DjangoObjectOnlyPermissions,)
60     filter_backends = (ObjectPermissionsFilter,)
61 
62     @action(detail=True, methods=["patch"])
63     def keep_alive(self, *_, **__):
64         """Increase the maximum duration of the session, up to the limit."""
65         session = self.get_object()
66 
67         new_duration = now() + timedelta(minutes=5) - session.created
68         duration_limit = timedelta(
69             seconds=settings.WORKSTATIONS_SESSION_DURATION_LIMIT
70         )
71 
72         if new_duration < duration_limit:
73             session.maximum_duration = new_duration
74             session.save()
75             return Response({"status": "session extended"})
76         else:
77             session.maximum_duration = duration_limit
78             session.save()
79             return Response(
80                 {"status": "session duration limit reached"},
81                 status=HTTP_400_BAD_REQUEST,
82             )
83 
84 
85 class WorkstationList(LoginRequiredMixin, PermissionListMixin, ListView):
86     model = Workstation
87     permission_required = (
88         f"{Workstation._meta.app_label}.view_{Workstation._meta.model_name}"
89     )
90 
91 
92 class WorkstationCreate(
93     LoginRequiredMixin, PermissionRequiredMixin, CreateView
94 ):
95     model = Workstation
96     form_class = WorkstationForm
97     permission_required = (
98         f"{Workstation._meta.app_label}.add_{Workstation._meta.model_name}"
99     )
100 
101     def form_valid(self, form):
102         response = super().form_valid(form)
103         self.object.add_editor(user=self.request.user)
104         return response
105 
106 
107 class WorkstationDetail(
108     LoginRequiredMixin, ObjectPermissionRequiredMixin, DetailView
109 ):
110     model = Workstation
111     permission_required = (
112         f"{Workstation._meta.app_label}.view_{Workstation._meta.model_name}"
113     )
114     raise_exception = True
115 
116 
117 class WorkstationUpdate(
118     LoginRequiredMixin, ObjectPermissionRequiredMixin, UpdateView
119 ):
120     model = Workstation
121     form_class = WorkstationForm
122     permission_required = (
123         f"{Workstation._meta.app_label}.change_{Workstation._meta.model_name}"
124     )
125     raise_exception = True
126 
127 
128 class WorkstationUsersAutocomplete(
129     LoginRequiredMixin, UserPassesTestMixin, autocomplete.Select2QuerySetView
130 ):
131     def test_func(self):
132         group_pks = (
133             Workstation.objects.all()
134             .select_related("editors_group")
135             .values_list("editors_group__pk", flat=True)
136         )
137         return (
138             self.request.user.is_superuser
139             or self.request.user.groups.filter(pk__in=group_pks).exists()
140         )
141 
142     def get_queryset(self):
143         qs = (
144             get_user_model()
145             .objects.all()
146             .order_by("username")
147             .exclude(username=settings.ANONYMOUS_USER_NAME)
148         )
149 
150         if self.q:
151             qs = qs.filter(username__istartswith=self.q)
152 
153         return qs
154 
155 
156 class WorkstationGroupUpdateMixin(
157     LoginRequiredMixin,
158     ObjectPermissionRequiredMixin,
159     SuccessMessageMixin,
160     FormView,
161 ):
162     template_name = "workstations/workstation_user_groups_form.html"
163     permission_required = (
164         f"{Workstation._meta.app_label}.change_{Workstation._meta.model_name}"
165     )
166     raise_exception = True
167 
168     def get_permission_object(self):
169         return self.workstation
170 
171     @property
172     def workstation(self):
173         return get_object_or_404(Workstation, slug=self.kwargs["slug"])
174 
175     def get_context_data(self, **kwargs):
176         context = super().get_context_data(**kwargs)
177         context.update(
178             {"object": self.workstation, "role": self.get_form().role}
179         )
180         return context
181 
182     def get_success_url(self):
183         return self.workstation.get_absolute_url()
184 
185     def form_valid(self, form):
186         form.add_or_remove_user(workstation=self.workstation)
187         return super().form_valid(form)
188 
189 
190 class WorkstationEditorsUpdate(WorkstationGroupUpdateMixin):
191     form_class = EditorsForm
192     success_message = "Editors successfully updated"
193 
194 
195 class WorkstationUsersUpdate(WorkstationGroupUpdateMixin):
196     form_class = UsersForm
197     success_message = "Users successfully updated"
198 
199 
200 class WorkstationImageCreate(
201     LoginRequiredMixin, ObjectPermissionRequiredMixin, CreateView
202 ):
203     model = WorkstationImage
204     form_class = WorkstationImageForm
205     permission_required = (
206         f"{Workstation._meta.app_label}.change_{Workstation._meta.model_name}"
207     )
208     raise_exception = True
209 
210     def get_context_data(self, **kwargs):
211         context = super().get_context_data(**kwargs)
212         context.update({"workstation": self.workstation})
213         return context
214 
215     def get_form_kwargs(self):
216         kwargs = super().get_form_kwargs()
217         kwargs.update({"user": self.request.user})
218         return kwargs
219 
220     @property
221     def workstation(self):
222         return get_object_or_404(Workstation, slug=self.kwargs["slug"])
223 
224     def get_permission_object(self):
225         return self.workstation
226 
227     def form_valid(self, form):
228         form.instance.creator = self.request.user
229         form.instance.workstation = self.workstation
230 
231         uploaded_file = form.cleaned_data["chunked_upload"][0]
232         form.instance.staged_image_uuid = uploaded_file.uuid
233 
234         return super().form_valid(form)
235 
236 
237 class WorkstationImageDetail(
238     LoginRequiredMixin, ObjectPermissionRequiredMixin, DetailView
239 ):
240     model = WorkstationImage
241     permission_required = f"{WorkstationImage._meta.app_label}.view_{WorkstationImage._meta.model_name}"
242     raise_exception = True
243 
244 
245 class WorkstationImageUpdate(
246     LoginRequiredMixin, ObjectPermissionRequiredMixin, UpdateView
247 ):
248     model = WorkstationImage
249     fields = ("initial_path", "http_port", "websocket_port")
250     template_name_suffix = "_update"
251     permission_required = f"{WorkstationImage._meta.app_label}.change_{WorkstationImage._meta.model_name}"
252     raise_exception = True
253 
254 
255 class SessionRedirectView(
256     LoginRequiredMixin, ObjectPermissionRequiredMixin, RedirectView
257 ):
258     permanent = False
259     permission_required = (
260         f"{Workstation._meta.app_label}.view_{Workstation._meta.model_name}"
261     )
262     raise_exception = True
263 
264     def get_permission_object(self):
265         return get_workstation_image_or_404(**self.kwargs).workstation
266 
267     def get_redirect_url(self, *args, **kwargs):
268         workstation_image = get_workstation_image_or_404(**kwargs)
269         session = get_or_create_active_session(
270             user=self.request.user, workstation_image=workstation_image
271         )
272 
273         url = session.get_absolute_url()
274 
275         qs = self.request.META.get("QUERY_STRING", "")
276         if qs:
277             url = f"{url}?{qs}"
278 
279         return url
280 
281 
282 class SessionCreate(
283     LoginRequiredMixin, ObjectPermissionRequiredMixin, CreateView
284 ):
285     model = Session
286     fields = []
287     permission_required = (
288         f"{Workstation._meta.app_label}.view_{Workstation._meta.model_name}"
289     )
290     raise_exception = True
291 
292     @property
293     def workstation(self):
294         return get_object_or_404(Workstation, slug=self.kwargs["slug"])
295 
296     def get_permission_object(self):
297         return self.workstation
298 
299     def form_valid(self, form):
300         form.instance.creator = self.request.user
301         form.instance.workstation_image = self.workstation.latest_ready_image
302         return super().form_valid(form)
303 
304 
305 class SessionUpdate(
306     LoginRequiredMixin, ObjectPermissionRequiredMixin, UpdateView
307 ):
308     model = Session
309     fields = ["user_finished"]
310     permission_required = (
311         f"{Session._meta.app_label}.change_{Session._meta.model_name}"
312     )
313     raise_exception = True
314 
315 
316 class SessionDetail(
317     LoginRequiredMixin, ObjectPermissionRequiredMixin, DetailView
318 ):
319     model = Session
320     permission_required = (
321         f"{Session._meta.app_label}.view_{Session._meta.model_name}"
322     )
323     raise_exception = True
324 
325 
326 def session_proxy(request, *, pk, path, **_):
327     """Return an internal redirect to the session instance if authorised."""
328     session = get_object_or_404(Session, pk=pk)
329     path = safe_join(f"/workstation-proxy/{session.hostname}", path)
330 
331     user = request.user
332     if session.creator != user:
333         raise PermissionDenied
334 
335     response = HttpResponse()
336     response["X-Accel-Redirect"] = path
337 
338     return response
339
```
--- 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/app/grandchallenge/workstations/views.py b/app/grandchallenge/workstations/views.py
--- a/app/grandchallenge/workstations/views.py
+++ b/app/grandchallenge/workstations/views.py
@@ -31,6 +31,7 @@
 from rest_framework.status import HTTP_400_BAD_REQUEST
 from rest_framework.viewsets import ReadOnlyModelViewSet
 from rest_framework_guardian.filters import ObjectPermissionsFilter
+from ua_parser.user_agent_parser import ParseUserAgent
 
 from grandchallenge.core.permissions.rest_framework import (
     DjangoObjectOnlyPermissions,
@@ -322,6 +323,45 @@
     )
     raise_exception = True
 
+    def _get_unsupported_browser_message(self):
+        user_agent = ParseUserAgent(
+            self.request.META.get("HTTP_USER_AGENT", "")
+        )
+
+        unsupported_browser = user_agent["family"].lower() not in [
+            "firefox",
+            "chrome",
+        ]
+
+        unsupported_chrome_version = (
+            user_agent["family"].lower() == "chrome"
+            and int(user_agent["major"]) < 79
+        )
+
+        if unsupported_browser:
+            unsupported_browser_message = (
+                "Unfortunately your browser is not supported. "
+                "Please try again with the latest version of Firefox or Chrome."
+            )
+        elif unsupported_chrome_version:
+            unsupported_browser_message = (
+                "Unfortunately your version of Chrome is not supported. "
+                "Please update to the latest version and try again."
+            )
+        else:
+            unsupported_browser_message = None
+
+        return unsupported_browser_message
+
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs)
+        context.update(
+            {
+                "unsupported_browser_message": self._get_unsupported_browser_message()
+            }
+        )
+        return context
+
 
 def session_proxy(request, *, pk, path, **_):
     """Return an internal redirect to the session instance if authorised."""

{"golden_diff": "diff --git a/app/grandchallenge/workstations/views.py b/app/grandchallenge/workstations/views.py\n--- a/app/grandchallenge/workstations/views.py\n+++ b/app/grandchallenge/workstations/views.py\n@@ -31,6 +31,7 @@\n from rest_framework.status import HTTP_400_BAD_REQUEST\n from rest_framework.viewsets import ReadOnlyModelViewSet\n from rest_framework_guardian.filters import ObjectPermissionsFilter\n+from ua_parser.user_agent_parser import ParseUserAgent\n \n from grandchallenge.core.permissions.rest_framework import (\n     DjangoObjectOnlyPermissions,\n@@ -322,6 +323,45 @@\n     )\n     raise_exception = True\n \n+    def _get_unsupported_browser_message(self):\n+        user_agent = ParseUserAgent(\n+            self.request.META.get(\"HTTP_USER_AGENT\", \"\")\n+        )\n+\n+        unsupported_browser = user_agent[\"family\"].lower() not in [\n+            \"firefox\",\n+            \"chrome\",\n+        ]\n+\n+        unsupported_chrome_version = (\n+            user_agent[\"family\"].lower() == \"chrome\"\n+            and int(user_agent[\"major\"]) < 79\n+        )\n+\n+        if unsupported_browser:\n+            unsupported_browser_message = (\n+                \"Unfortunately your browser is not supported. \"\n+                \"Please try again with the latest version of Firefox or Chrome.\"\n+            )\n+        elif unsupported_chrome_version:\n+            unsupported_browser_message = (\n+                \"Unfortunately your version of Chrome is not supported. \"\n+                \"Please update to the latest version and try again.\"\n+            )\n+        else:\n+            unsupported_browser_message = None\n+\n+        return unsupported_browser_message\n+\n+    def get_context_data(self, **kwargs):\n+        context = super().get_context_data(**kwargs)\n+        context.update(\n+            {\n+                \"unsupported_browser_message\": self._get_unsupported_browser_message()\n+            }\n+        )\n+        return context\n+\n \n def session_proxy(request, *, pk, path, **_):\n     \"\"\"Return an internal redirect to the session instance if authorised.\"\"\"\n", "issue": "Add a check for browser on reader study session detail\nDon't use internet explorer or an old version of chrome.\n", "before_files": [{"content": "from datetime import timedelta\n\nfrom dal import autocomplete\nfrom django.conf import settings\nfrom django.contrib.auth import get_user_model\nfrom django.contrib.auth.mixins import (\n    PermissionRequiredMixin,\n    UserPassesTestMixin,\n)\nfrom django.contrib.messages.views import SuccessMessageMixin\nfrom django.core.exceptions import PermissionDenied\nfrom django.http import HttpResponse\nfrom django.shortcuts import get_object_or_404\nfrom django.utils._os import safe_join\nfrom django.utils.timezone import now\nfrom django.views.generic import (\n    CreateView,\n    DetailView,\n    FormView,\n    ListView,\n    RedirectView,\n    UpdateView,\n)\nfrom guardian.mixins import (\n    LoginRequiredMixin,\n    PermissionListMixin,\n    PermissionRequiredMixin as ObjectPermissionRequiredMixin,\n)\nfrom rest_framework.decorators import action\nfrom rest_framework.response import Response\nfrom rest_framework.status import HTTP_400_BAD_REQUEST\nfrom rest_framework.viewsets import ReadOnlyModelViewSet\nfrom rest_framework_guardian.filters import ObjectPermissionsFilter\n\nfrom grandchallenge.core.permissions.rest_framework import (\n    DjangoObjectOnlyPermissions,\n)\nfrom grandchallenge.workstations.forms import (\n    EditorsForm,\n    UsersForm,\n    WorkstationForm,\n    WorkstationImageForm,\n)\nfrom grandchallenge.workstations.models import (\n    Session,\n    Workstation,\n    WorkstationImage,\n)\nfrom grandchallenge.workstations.serializers import SessionSerializer\nfrom grandchallenge.workstations.utils import (\n    get_or_create_active_session,\n    get_workstation_image_or_404,\n)\n\n\nclass SessionViewSet(ReadOnlyModelViewSet):\n    queryset = Session.objects.all()\n    serializer_class = SessionSerializer\n    permission_classes = (DjangoObjectOnlyPermissions,)\n    filter_backends = (ObjectPermissionsFilter,)\n\n    @action(detail=True, methods=[\"patch\"])\n    def keep_alive(self, *_, **__):\n        \"\"\"Increase the maximum duration of the session, up to the limit.\"\"\"\n        session = self.get_object()\n\n        new_duration = now() + timedelta(minutes=5) - session.created\n        duration_limit = timedelta(\n            seconds=settings.WORKSTATIONS_SESSION_DURATION_LIMIT\n        )\n\n        if new_duration < duration_limit:\n            session.maximum_duration = new_duration\n            session.save()\n            return Response({\"status\": \"session extended\"})\n        else:\n            session.maximum_duration = duration_limit\n            session.save()\n            return Response(\n                {\"status\": \"session duration limit reached\"},\n                status=HTTP_400_BAD_REQUEST,\n            )\n\n\nclass WorkstationList(LoginRequiredMixin, PermissionListMixin, ListView):\n    model = Workstation\n    permission_required = (\n        f\"{Workstation._meta.app_label}.view_{Workstation._meta.model_name}\"\n    )\n\n\nclass WorkstationCreate(\n    LoginRequiredMixin, PermissionRequiredMixin, CreateView\n):\n    model = Workstation\n    form_class = WorkstationForm\n    permission_required = (\n        f\"{Workstation._meta.app_label}.add_{Workstation._meta.model_name}\"\n    )\n\n    def form_valid(self, form):\n        response = super().form_valid(form)\n        self.object.add_editor(user=self.request.user)\n        return response\n\n\nclass WorkstationDetail(\n    LoginRequiredMixin, ObjectPermissionRequiredMixin, DetailView\n):\n    model = Workstation\n    permission_required = (\n        f\"{Workstation._meta.app_label}.view_{Workstation._meta.model_name}\"\n    )\n    raise_exception = True\n\n\nclass WorkstationUpdate(\n    LoginRequiredMixin, ObjectPermissionRequiredMixin, UpdateView\n):\n    model = Workstation\n    form_class = WorkstationForm\n    permission_required = (\n        f\"{Workstation._meta.app_label}.change_{Workstation._meta.model_name}\"\n    )\n    raise_exception = True\n\n\nclass WorkstationUsersAutocomplete(\n    LoginRequiredMixin, UserPassesTestMixin, autocomplete.Select2QuerySetView\n):\n    def test_func(self):\n        group_pks = (\n            Workstation.objects.all()\n            .select_related(\"editors_group\")\n            .values_list(\"editors_group__pk\", flat=True)\n        )\n        return (\n            self.request.user.is_superuser\n            or self.request.user.groups.filter(pk__in=group_pks).exists()\n        )\n\n    def get_queryset(self):\n        qs = (\n            get_user_model()\n            .objects.all()\n            .order_by(\"username\")\n            .exclude(username=settings.ANONYMOUS_USER_NAME)\n        )\n\n        if self.q:\n            qs = qs.filter(username__istartswith=self.q)\n\n        return qs\n\n\nclass WorkstationGroupUpdateMixin(\n    LoginRequiredMixin,\n    ObjectPermissionRequiredMixin,\n    SuccessMessageMixin,\n    FormView,\n):\n    template_name = \"workstations/workstation_user_groups_form.html\"\n    permission_required = (\n        f\"{Workstation._meta.app_label}.change_{Workstation._meta.model_name}\"\n    )\n    raise_exception = True\n\n    def get_permission_object(self):\n        return self.workstation\n\n    @property\n    def workstation(self):\n        return get_object_or_404(Workstation, slug=self.kwargs[\"slug\"])\n\n    def get_context_data(self, **kwargs):\n        context = super().get_context_data(**kwargs)\n        context.update(\n            {\"object\": self.workstation, \"role\": self.get_form().role}\n        )\n        return context\n\n    def get_success_url(self):\n        return self.workstation.get_absolute_url()\n\n    def form_valid(self, form):\n        form.add_or_remove_user(workstation=self.workstation)\n        return super().form_valid(form)\n\n\nclass WorkstationEditorsUpdate(WorkstationGroupUpdateMixin):\n    form_class = EditorsForm\n    success_message = \"Editors successfully updated\"\n\n\nclass WorkstationUsersUpdate(WorkstationGroupUpdateMixin):\n    form_class = UsersForm\n    success_message = \"Users successfully updated\"\n\n\nclass WorkstationImageCreate(\n    LoginRequiredMixin, ObjectPermissionRequiredMixin, CreateView\n):\n    model = WorkstationImage\n    form_class = WorkstationImageForm\n    permission_required = (\n        f\"{Workstation._meta.app_label}.change_{Workstation._meta.model_name}\"\n    )\n    raise_exception = True\n\n    def get_context_data(self, **kwargs):\n        context = super().get_context_data(**kwargs)\n        context.update({\"workstation\": self.workstation})\n        return context\n\n    def get_form_kwargs(self):\n        kwargs = super().get_form_kwargs()\n        kwargs.update({\"user\": self.request.user})\n        return kwargs\n\n    @property\n    def workstation(self):\n        return get_object_or_404(Workstation, slug=self.kwargs[\"slug\"])\n\n    def get_permission_object(self):\n        return self.workstation\n\n    def form_valid(self, form):\n        form.instance.creator = self.request.user\n        form.instance.workstation = self.workstation\n\n        uploaded_file = form.cleaned_data[\"chunked_upload\"][0]\n        form.instance.staged_image_uuid = uploaded_file.uuid\n\n        return super().form_valid(form)\n\n\nclass WorkstationImageDetail(\n    LoginRequiredMixin, ObjectPermissionRequiredMixin, DetailView\n):\n    model = WorkstationImage\n    permission_required = f\"{WorkstationImage._meta.app_label}.view_{WorkstationImage._meta.model_name}\"\n    raise_exception = True\n\n\nclass WorkstationImageUpdate(\n    LoginRequiredMixin, ObjectPermissionRequiredMixin, UpdateView\n):\n    model = WorkstationImage\n    fields = (\"initial_path\", \"http_port\", \"websocket_port\")\n    template_name_suffix = \"_update\"\n    permission_required = f\"{WorkstationImage._meta.app_label}.change_{WorkstationImage._meta.model_name}\"\n    raise_exception = True\n\n\nclass SessionRedirectView(\n    LoginRequiredMixin, ObjectPermissionRequiredMixin, RedirectView\n):\n    permanent = False\n    permission_required = (\n        f\"{Workstation._meta.app_label}.view_{Workstation._meta.model_name}\"\n    )\n    raise_exception = True\n\n    def get_permission_object(self):\n        return get_workstation_image_or_404(**self.kwargs).workstation\n\n    def get_redirect_url(self, *args, **kwargs):\n        workstation_image = get_workstation_image_or_404(**kwargs)\n        session = get_or_create_active_session(\n            user=self.request.user, workstation_image=workstation_image\n        )\n\n        url = session.get_absolute_url()\n\n        qs = self.request.META.get(\"QUERY_STRING\", \"\")\n        if qs:\n            url = f\"{url}?{qs}\"\n\n        return url\n\n\nclass SessionCreate(\n    LoginRequiredMixin, ObjectPermissionRequiredMixin, CreateView\n):\n    model = Session\n    fields = []\n    permission_required = (\n        f\"{Workstation._meta.app_label}.view_{Workstation._meta.model_name}\"\n    )\n    raise_exception = True\n\n    @property\n    def workstation(self):\n        return get_object_or_404(Workstation, slug=self.kwargs[\"slug\"])\n\n    def get_permission_object(self):\n        return self.workstation\n\n    def form_valid(self, form):\n        form.instance.creator = self.request.user\n        form.instance.workstation_image = self.workstation.latest_ready_image\n        return super().form_valid(form)\n\n\nclass SessionUpdate(\n    LoginRequiredMixin, ObjectPermissionRequiredMixin, UpdateView\n):\n    model = Session\n    fields = [\"user_finished\"]\n    permission_required = (\n        f\"{Session._meta.app_label}.change_{Session._meta.model_name}\"\n    )\n    raise_exception = True\n\n\nclass SessionDetail(\n    LoginRequiredMixin, ObjectPermissionRequiredMixin, DetailView\n):\n    model = Session\n    permission_required = (\n        f\"{Session._meta.app_label}.view_{Session._meta.model_name}\"\n    )\n    raise_exception = True\n\n\ndef session_proxy(request, *, pk, path, **_):\n    \"\"\"Return an internal redirect to the session instance if authorised.\"\"\"\n    session = get_object_or_404(Session, pk=pk)\n    path = safe_join(f\"/workstation-proxy/{session.hostname}\", path)\n\n    user = request.user\n    if session.creator != user:\n        raise PermissionDenied\n\n    response = HttpResponse()\n    response[\"X-Accel-Redirect\"] = path\n\n    return response\n", "path": "app/grandchallenge/workstations/views.py"}], "after_files": [{"content": "from datetime import timedelta\n\nfrom dal import autocomplete\nfrom django.conf import settings\nfrom django.contrib.auth import get_user_model\nfrom django.contrib.auth.mixins import (\n    PermissionRequiredMixin,\n    UserPassesTestMixin,\n)\nfrom django.contrib.messages.views import SuccessMessageMixin\nfrom django.core.exceptions import PermissionDenied\nfrom django.http import HttpResponse\nfrom django.shortcuts import get_object_or_404\nfrom django.utils._os import safe_join\nfrom django.utils.timezone import now\nfrom django.views.generic import (\n    CreateView,\n    DetailView,\n    FormView,\n    ListView,\n    RedirectView,\n    UpdateView,\n)\nfrom guardian.mixins import (\n    LoginRequiredMixin,\n    PermissionListMixin,\n    PermissionRequiredMixin as ObjectPermissionRequiredMixin,\n)\nfrom rest_framework.decorators import action\nfrom rest_framework.response import Response\nfrom rest_framework.status import HTTP_400_BAD_REQUEST\nfrom rest_framework.viewsets import ReadOnlyModelViewSet\nfrom rest_framework_guardian.filters import ObjectPermissionsFilter\nfrom ua_parser.user_agent_parser import ParseUserAgent\n\nfrom grandchallenge.core.permissions.rest_framework import (\n    DjangoObjectOnlyPermissions,\n)\nfrom grandchallenge.workstations.forms import (\n    EditorsForm,\n    UsersForm,\n    WorkstationForm,\n    WorkstationImageForm,\n)\nfrom grandchallenge.workstations.models import (\n    Session,\n    Workstation,\n    WorkstationImage,\n)\nfrom grandchallenge.workstations.serializers import SessionSerializer\nfrom grandchallenge.workstations.utils import (\n    get_or_create_active_session,\n    get_workstation_image_or_404,\n)\n\n\nclass SessionViewSet(ReadOnlyModelViewSet):\n    queryset = Session.objects.all()\n    serializer_class = SessionSerializer\n    permission_classes = (DjangoObjectOnlyPermissions,)\n    filter_backends = (ObjectPermissionsFilter,)\n\n    @action(detail=True, methods=[\"patch\"])\n    def keep_alive(self, *_, **__):\n        \"\"\"Increase the maximum duration of the session, up to the limit.\"\"\"\n        session = self.get_object()\n\n        new_duration = now() + timedelta(minutes=5) - session.created\n        duration_limit = timedelta(\n            seconds=settings.WORKSTATIONS_SESSION_DURATION_LIMIT\n        )\n\n        if new_duration < duration_limit:\n            session.maximum_duration = new_duration\n            session.save()\n            return Response({\"status\": \"session extended\"})\n        else:\n            session.maximum_duration = duration_limit\n            session.save()\n            return Response(\n                {\"status\": \"session duration limit reached\"},\n                status=HTTP_400_BAD_REQUEST,\n            )\n\n\nclass WorkstationList(LoginRequiredMixin, PermissionListMixin, ListView):\n    model = Workstation\n    permission_required = (\n        f\"{Workstation._meta.app_label}.view_{Workstation._meta.model_name}\"\n    )\n\n\nclass WorkstationCreate(\n    LoginRequiredMixin, PermissionRequiredMixin, CreateView\n):\n    model = Workstation\n    form_class = WorkstationForm\n    permission_required = (\n        f\"{Workstation._meta.app_label}.add_{Workstation._meta.model_name}\"\n    )\n\n    def form_valid(self, form):\n        response = super().form_valid(form)\n        self.object.add_editor(user=self.request.user)\n        return response\n\n\nclass WorkstationDetail(\n    LoginRequiredMixin, ObjectPermissionRequiredMixin, DetailView\n):\n    model = Workstation\n    permission_required = (\n        f\"{Workstation._meta.app_label}.view_{Workstation._meta.model_name}\"\n    )\n    raise_exception = True\n\n\nclass WorkstationUpdate(\n    LoginRequiredMixin, ObjectPermissionRequiredMixin, UpdateView\n):\n    model = Workstation\n    form_class = WorkstationForm\n    permission_required = (\n        f\"{Workstation._meta.app_label}.change_{Workstation._meta.model_name}\"\n    )\n    raise_exception = True\n\n\nclass WorkstationUsersAutocomplete(\n    LoginRequiredMixin, UserPassesTestMixin, autocomplete.Select2QuerySetView\n):\n    def test_func(self):\n        group_pks = (\n            Workstation.objects.all()\n            .select_related(\"editors_group\")\n            .values_list(\"editors_group__pk\", flat=True)\n        )\n        return (\n            self.request.user.is_superuser\n            or self.request.user.groups.filter(pk__in=group_pks).exists()\n        )\n\n    def get_queryset(self):\n        qs = (\n            get_user_model()\n            .objects.all()\n            .order_by(\"username\")\n            .exclude(username=settings.ANONYMOUS_USER_NAME)\n        )\n\n        if self.q:\n            qs = qs.filter(username__istartswith=self.q)\n\n        return qs\n\n\nclass WorkstationGroupUpdateMixin(\n    LoginRequiredMixin,\n    ObjectPermissionRequiredMixin,\n    SuccessMessageMixin,\n    FormView,\n):\n    template_name = \"workstations/workstation_user_groups_form.html\"\n    permission_required = (\n        f\"{Workstation._meta.app_label}.change_{Workstation._meta.model_name}\"\n    )\n    raise_exception = True\n\n    def get_permission_object(self):\n        return self.workstation\n\n    @property\n    def workstation(self):\n        return get_object_or_404(Workstation, slug=self.kwargs[\"slug\"])\n\n    def get_context_data(self, **kwargs):\n        context = super().get_context_data(**kwargs)\n        context.update(\n            {\"object\": self.workstation, \"role\": self.get_form().role}\n        )\n        return context\n\n    def get_success_url(self):\n        return self.workstation.get_absolute_url()\n\n    def form_valid(self, form):\n        form.add_or_remove_user(workstation=self.workstation)\n        return super().form_valid(form)\n\n\nclass WorkstationEditorsUpdate(WorkstationGroupUpdateMixin):\n    form_class = EditorsForm\n    success_message = \"Editors successfully updated\"\n\n\nclass WorkstationUsersUpdate(WorkstationGroupUpdateMixin):\n    form_class = UsersForm\n    success_message = \"Users successfully updated\"\n\n\nclass WorkstationImageCreate(\n    LoginRequiredMixin, ObjectPermissionRequiredMixin, CreateView\n):\n    model = WorkstationImage\n    form_class = WorkstationImageForm\n    permission_required = (\n        f\"{Workstation._meta.app_label}.change_{Workstation._meta.model_name}\"\n    )\n    raise_exception = True\n\n    def get_context_data(self, **kwargs):\n        context = super().get_context_data(**kwargs)\n        context.update({\"workstation\": self.workstation})\n        return context\n\n    def get_form_kwargs(self):\n        kwargs = super().get_form_kwargs()\n        kwargs.update({\"user\": self.request.user})\n        return kwargs\n\n    @property\n    def workstation(self):\n        return get_object_or_404(Workstation, slug=self.kwargs[\"slug\"])\n\n    def get_permission_object(self):\n        return self.workstation\n\n    def form_valid(self, form):\n        form.instance.creator = self.request.user\n        form.instance.workstation = self.workstation\n\n        uploaded_file = form.cleaned_data[\"chunked_upload\"][0]\n        form.instance.staged_image_uuid = uploaded_file.uuid\n\n        return super().form_valid(form)\n\n\nclass WorkstationImageDetail(\n    LoginRequiredMixin, ObjectPermissionRequiredMixin, DetailView\n):\n    model = WorkstationImage\n    permission_required = f\"{WorkstationImage._meta.app_label}.view_{WorkstationImage._meta.model_name}\"\n    raise_exception = True\n\n\nclass WorkstationImageUpdate(\n    LoginRequiredMixin, ObjectPermissionRequiredMixin, UpdateView\n):\n    model = WorkstationImage\n    fields = (\"initial_path\", \"http_port\", \"websocket_port\")\n    template_name_suffix = \"_update\"\n    permission_required = f\"{WorkstationImage._meta.app_label}.change_{WorkstationImage._meta.model_name}\"\n    raise_exception = True\n\n\nclass SessionRedirectView(\n    LoginRequiredMixin, ObjectPermissionRequiredMixin, RedirectView\n):\n    permanent = False\n    permission_required = (\n        f\"{Workstation._meta.app_label}.view_{Workstation._meta.model_name}\"\n    )\n    raise_exception = True\n\n    def get_permission_object(self):\n        return get_workstation_image_or_404(**self.kwargs).workstation\n\n    def get_redirect_url(self, *args, **kwargs):\n        workstation_image = get_workstation_image_or_404(**kwargs)\n        session = get_or_create_active_session(\n            user=self.request.user, workstation_image=workstation_image\n        )\n\n        url = session.get_absolute_url()\n\n        qs = self.request.META.get(\"QUERY_STRING\", \"\")\n        if qs:\n            url = f\"{url}?{qs}\"\n\n        return url\n\n\nclass SessionCreate(\n    LoginRequiredMixin, ObjectPermissionRequiredMixin, CreateView\n):\n    model = Session\n    fields = []\n    permission_required = (\n        f\"{Workstation._meta.app_label}.view_{Workstation._meta.model_name}\"\n    )\n    raise_exception = True\n\n    @property\n    def workstation(self):\n        return get_object_or_404(Workstation, slug=self.kwargs[\"slug\"])\n\n    def get_permission_object(self):\n        return self.workstation\n\n    def form_valid(self, form):\n        form.instance.creator = self.request.user\n        form.instance.workstation_image = self.workstation.latest_ready_image\n        return super().form_valid(form)\n\n\nclass SessionUpdate(\n    LoginRequiredMixin, ObjectPermissionRequiredMixin, UpdateView\n):\n    model = Session\n    fields = [\"user_finished\"]\n    permission_required = (\n        f\"{Session._meta.app_label}.change_{Session._meta.model_name}\"\n    )\n    raise_exception = True\n\n\nclass SessionDetail(\n    LoginRequiredMixin, ObjectPermissionRequiredMixin, DetailView\n):\n    model = Session\n    permission_required = (\n        f\"{Session._meta.app_label}.view_{Session._meta.model_name}\"\n    )\n    raise_exception = True\n\n    def _get_unsupported_browser_message(self):\n        user_agent = ParseUserAgent(\n            self.request.META.get(\"HTTP_USER_AGENT\", \"\")\n        )\n\n        unsupported_browser = user_agent[\"family\"].lower() not in [\n            \"firefox\",\n            \"chrome\",\n        ]\n\n        unsupported_chrome_version = (\n            user_agent[\"family\"].lower() == \"chrome\"\n            and int(user_agent[\"major\"]) < 79\n        )\n\n        if unsupported_browser:\n            unsupported_browser_message = (\n                \"Unfortunately your browser is not supported. \"\n                \"Please try again with the latest version of Firefox or Chrome.\"\n            )\n        elif unsupported_chrome_version:\n            unsupported_browser_message = (\n                \"Unfortunately your version of Chrome is not supported. \"\n                \"Please update to the latest version and try again.\"\n            )\n        else:\n            unsupported_browser_message = None\n\n        return unsupported_browser_message\n\n    def get_context_data(self, **kwargs):\n        context = super().get_context_data(**kwargs)\n        context.update(\n            {\n                \"unsupported_browser_message\": self._get_unsupported_browser_message()\n            }\n        )\n        return context\n\n\ndef session_proxy(request, *, pk, path, **_):\n    \"\"\"Return an internal redirect to the session instance if authorised.\"\"\"\n    session = get_object_or_404(Session, pk=pk)\n    path = safe_join(f\"/workstation-proxy/{session.hostname}\", path)\n\n    user = request.user\n    if session.creator != user:\n        raise PermissionDenied\n\n    response = HttpResponse()\n    response[\"X-Accel-Redirect\"] = path\n\n    return response\n", "path": "app/grandchallenge/workstations/views.py"}]}