We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Remove unused CIRRUS environment variables
These variables are now deprecated.

https://github.com/comic/grand-challenge.org/blob/30875fd388f2ad14212cf57c0caa8b9efcba19d9/app/grandchallenge/workstations/models.py#L201

--- 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/models.py`
Content:
```
1 from datetime import timedelta, datetime
2 from urllib.parse import unquote, urljoin
3 
4 from django.conf import settings
5 from django.core.validators import MaxValueValidator, RegexValidator
6 from django.db import models
7 from django_extensions.db.models import TitleSlugDescriptionModel
8 from rest_framework.authtoken.models import Token
9 from simple_history.models import HistoricalRecords
10 
11 from grandchallenge.challenges.models import get_logo_path
12 from grandchallenge.container_exec.backends.docker import Service
13 from grandchallenge.container_exec.models import ContainerImageModel
14 from grandchallenge.container_exec.tasks import start_service, stop_service
15 from grandchallenge.core.models import UUIDModel
16 from grandchallenge.subdomains.utils import reverse
17 
18 __doc__ = """
19 Workstations are used to view, annotate and upload images to grand challenge.
20 A `workstation admin` is able to upload a ``WorkstationImage``, which is a docker container image.
21 A ``WorkstationImage`` expose a http and, optionally, a websocket port.
22 A `workstation user` can then launch a workstation ``Session`` for a particular ``WorkstationImage``.
23 
24 When a new session is started, a new container instance of the selected ``WorkstationImage`` is lauched on the docker host.
25 The connection to the container will be proxied, and only accessible to the user that created the session.
26 The proxy will map the http and websocket connections from the user to the running instance, which is mapped by the container hostname.
27 The container instance will have the users API token set in the environment, so that it is able to interact with the grand challenge API as this user.
28 The user is able to stop the container, otherwise it will be terminated after ``maxmium_duration`` is reached.
29 """
30 
31 
32 class Workstation(UUIDModel, TitleSlugDescriptionModel):
33     """ This model holds the title and description of a workstation. """
34 
35     logo = models.ImageField(upload_to=get_logo_path)
36 
37     @property
38     def latest_ready_image(self):
39         """
40         Returns
41         -------
42             The most recent container image for this workstation
43         """
44         return (
45             self.workstationimage_set.filter(ready=True)
46             .order_by("-created")
47             .first()
48         )
49 
50     def __str__(self):
51         return f"Workstation {self.title}"
52 
53     def get_absolute_url(self):
54         return reverse("workstations:detail", kwargs={"slug": self.slug})
55 
56 
57 class WorkstationImage(UUIDModel, ContainerImageModel):
58     """
59     A ``WorkstationImage`` is a docker container image of a workstation.
60 
61     Parameters
62     ----------
63     workstation
64         A ``Workstation`` can have multiple ``WorkstationImage``, that
65         represent different versions of a workstation
66     http_port
67         This container will expose a http server on this port
68     websocket_port
69         This container will expose a websocket on this port. Any relative url
70         that starts with ``/mlab4d4c4142`` will be proxied to this port.
71     initial_path
72         The initial path that users will navigate to in order to load the
73         workstation
74     """
75 
76     workstation = models.ForeignKey(Workstation, on_delete=models.CASCADE)
77     http_port = models.PositiveIntegerField(
78         default=8080, validators=[MaxValueValidator(2 ** 16 - 1)]
79     )
80     websocket_port = models.PositiveIntegerField(
81         default=4114, validators=[MaxValueValidator(2 ** 16 - 1)]
82     )
83     initial_path = models.CharField(
84         max_length=256,
85         default="Applications/GrandChallengeViewer/index.html",
86         blank=True,
87         validators=[
88             RegexValidator(
89                 regex=r"^(?:[^/][^\s]*)\Z",
90                 message="This path is invalid, it must not start with a /",
91             )
92         ],
93     )
94 
95     def __str__(self):
96         return f"Workstation Image {self.pk}"
97 
98     def get_absolute_url(self):
99         return reverse(
100             "workstations:image-detail",
101             kwargs={"slug": self.workstation.slug, "pk": self.pk},
102         )
103 
104 
105 class Session(UUIDModel):
106     """
107     Tracks who has launched workstation images. The ``WorkstationImage`` will
108     be launched as a ``Service``. The ``Session`` is responsible for starting
109     and stopping the ``Service``.
110 
111     Parameters
112     ----------
113 
114     status
115         Stores what has happened with the service, is it running, errored, etc?
116     creator
117         Who created the session? This is also the only user that should be able
118         to access the launched service.
119     workstation_image
120         The container image that will be launched by this ``Session``.
121     maximum_duration
122         The maximum time that the service can be active before it is terminated
123     user_finished
124         Indicates if the user has chosen to end the session early
125     history
126         The history of this Session
127     """
128 
129     QUEUED = 0
130     STARTED = 1
131     RUNNING = 2
132     FAILED = 3
133     STOPPED = 4
134 
135     # These should match the values in session.js
136     STATUS_CHOICES = (
137         (QUEUED, "Queued"),
138         (STARTED, "Started"),
139         (RUNNING, "Running"),
140         (FAILED, "Failed"),
141         (STOPPED, "Stopped"),
142     )
143 
144     status = models.PositiveSmallIntegerField(
145         choices=STATUS_CHOICES, default=QUEUED
146     )
147     creator = models.ForeignKey(
148         settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL
149     )
150     workstation_image = models.ForeignKey(
151         WorkstationImage, on_delete=models.CASCADE
152     )
153     maximum_duration = models.DurationField(default=timedelta(minutes=10))
154     user_finished = models.BooleanField(default=False)
155     history = HistoricalRecords()
156 
157     def __str__(self):
158         return f"Session {self.pk}"
159 
160     @property
161     def task_kwargs(self) -> dict:
162         """
163         Returns
164         -------
165             The kwargs that need to be passed to celery to get this object
166         """
167         return {
168             "app_label": self._meta.app_label,
169             "model_name": self._meta.model_name,
170             "pk": self.pk,
171         }
172 
173     @property
174     def hostname(self) -> str:
175         """
176         Returns
177         -------
178             The unique hostname for this session
179         """
180         return (
181             f"{self.pk}.{self._meta.model_name}.{self._meta.app_label}".lower()
182         )
183 
184     @property
185     def expires_at(self) -> datetime:
186         """
187         Returns
188         -------
189             The time when this session expires.
190         """
191         return self.created + self.maximum_duration
192 
193     @property
194     def environment(self) -> dict:
195         """
196         Returns
197         -------
198             The environment variables that should be set on the container.
199         """
200         env = {
201             "GRAND_CHALLENGE_PROXY_URL_MAPPINGS": "",
202             "GRAND_CHALLENGE_QUERY_IMAGE_URL": unquote(
203                 reverse("api:image-detail", kwargs={"pk": "{key}"})
204             ),
205         }
206 
207         if self.creator:
208             env.update(
209                 {
210                     "GRAND_CHALLENGE_AUTHORIZATION": f"TOKEN {Token.objects.get_or_create(user=self.creator)[0].key}"
211                 }
212             )
213 
214         if settings.DEBUG:
215             # Allow the container to communicate with the dev environment
216             env.update({"GRAND_CHALLENGE_UNSAFE": "True"})
217 
218         return env
219 
220     @property
221     def service(self) -> Service:
222         """
223         Returns
224         -------
225             The service for this session, could be active or inactive.
226         """
227         return Service(
228             job_id=self.pk,
229             job_model=f"{self._meta.app_label}-{self._meta.model_name}",
230             exec_image=self.workstation_image.image,
231             exec_image_sha256=self.workstation_image.image_sha256,
232         )
233 
234     @property
235     def workstation_url(self) -> str:
236         """
237         Returns
238         -------
239             The url that users will use to access the workstation instance.
240         """
241         return urljoin(
242             self.get_absolute_url(), self.workstation_image.initial_path
243         )
244 
245     def start(self) -> None:
246         """
247         Starts the service for this session, ensuring that the
248         ``workstation_image`` is ready to be used and that
249         ``WORKSTATIONS_MAXIMUM_SESSIONS`` has not been reached.
250 
251         Raises
252         ------
253         RunTimeError
254             If the service cannot be started.
255 
256         Returns
257         -------
258         """
259         try:
260             if not self.workstation_image.ready:
261                 raise RuntimeError("Workstation image was not ready")
262 
263             if (
264                 Session.objects.all()
265                 .filter(status__in=[Session.RUNNING, Session.STARTED])
266                 .count()
267                 >= settings.WORKSTATIONS_MAXIMUM_SESSIONS
268             ):
269                 raise RuntimeError("Too many sessions are running")
270 
271             self.service.start(
272                 http_port=self.workstation_image.http_port,
273                 websocket_port=self.workstation_image.websocket_port,
274                 hostname=self.hostname,
275                 environment=self.environment,
276             )
277             self.update_status(status=self.STARTED)
278         except RuntimeError:
279             self.update_status(status=self.FAILED)
280 
281     def stop(self) -> None:
282         """
283         Stops the service for this session, cleaning up all of the containers.
284 
285         Returns
286         -------
287         """
288         self.service.stop_and_cleanup()
289         self.update_status(status=self.STOPPED)
290 
291     def update_status(self, *, status: STATUS_CHOICES) -> None:
292         """
293         Updates the status of this session.
294 
295         Parameters
296         ----------
297         status
298             The new status for this session.
299 
300         Returns
301         -------
302         """
303         self.status = status
304         self.save()
305 
306     def get_absolute_url(self):
307         return reverse(
308             "workstations:session-detail",
309             kwargs={
310                 "slug": self.workstation_image.workstation.slug,
311                 "pk": self.pk,
312             },
313         )
314 
315     def save(self, *args, **kwargs) -> None:
316         """
317         Saves the session instance, starting or stopping the service if needed.
318 
319         Returns
320         -------
321         """
322         created = self._state.adding
323 
324         super().save(*args, **kwargs)
325 
326         if created:
327             start_service.apply_async(kwargs=self.task_kwargs)
328         elif self.user_finished and self.status != self.STOPPED:
329             stop_service.apply_async(kwargs=self.task_kwargs)
330
```
--- 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/models.py b/app/grandchallenge/workstations/models.py
--- a/app/grandchallenge/workstations/models.py
+++ b/app/grandchallenge/workstations/models.py
@@ -197,12 +197,7 @@
         -------
             The environment variables that should be set on the container.
         """
-        env = {
-            "GRAND_CHALLENGE_PROXY_URL_MAPPINGS": "",
-            "GRAND_CHALLENGE_QUERY_IMAGE_URL": unquote(
-                reverse("api:image-detail", kwargs={"pk": "{key}"})
-            ),
-        }
+        env = {"GRAND_CHALLENGE_API_ROOT": unquote(reverse("api:api-root"))}
 
         if self.creator:
             env.update(

{"golden_diff": "diff --git a/app/grandchallenge/workstations/models.py b/app/grandchallenge/workstations/models.py\n--- a/app/grandchallenge/workstations/models.py\n+++ b/app/grandchallenge/workstations/models.py\n@@ -197,12 +197,7 @@\n         -------\n             The environment variables that should be set on the container.\n         \"\"\"\n-        env = {\n-            \"GRAND_CHALLENGE_PROXY_URL_MAPPINGS\": \"\",\n-            \"GRAND_CHALLENGE_QUERY_IMAGE_URL\": unquote(\n-                reverse(\"api:image-detail\", kwargs={\"pk\": \"{key}\"})\n-            ),\n-        }\n+        env = {\"GRAND_CHALLENGE_API_ROOT\": unquote(reverse(\"api:api-root\"))}\n \n         if self.creator:\n             env.update(\n", "issue": "Remove unused CIRRUS environment variables\nThese variables are now deprecated.\r\n\r\nhttps://github.com/comic/grand-challenge.org/blob/30875fd388f2ad14212cf57c0caa8b9efcba19d9/app/grandchallenge/workstations/models.py#L201\n", "before_files": [{"content": "from datetime import timedelta, datetime\nfrom urllib.parse import unquote, urljoin\n\nfrom django.conf import settings\nfrom django.core.validators import MaxValueValidator, RegexValidator\nfrom django.db import models\nfrom django_extensions.db.models import TitleSlugDescriptionModel\nfrom rest_framework.authtoken.models import Token\nfrom simple_history.models import HistoricalRecords\n\nfrom grandchallenge.challenges.models import get_logo_path\nfrom grandchallenge.container_exec.backends.docker import Service\nfrom grandchallenge.container_exec.models import ContainerImageModel\nfrom grandchallenge.container_exec.tasks import start_service, stop_service\nfrom grandchallenge.core.models import UUIDModel\nfrom grandchallenge.subdomains.utils import reverse\n\n__doc__ = \"\"\"\nWorkstations are used to view, annotate and upload images to grand challenge.\nA `workstation admin` is able to upload a ``WorkstationImage``, which is a docker container image.\nA ``WorkstationImage`` expose a http and, optionally, a websocket port.\nA `workstation user` can then launch a workstation ``Session`` for a particular ``WorkstationImage``.\n\nWhen a new session is started, a new container instance of the selected ``WorkstationImage`` is lauched on the docker host.\nThe connection to the container will be proxied, and only accessible to the user that created the session.\nThe proxy will map the http and websocket connections from the user to the running instance, which is mapped by the container hostname.\nThe container instance will have the users API token set in the environment, so that it is able to interact with the grand challenge API as this user.\nThe user is able to stop the container, otherwise it will be terminated after ``maxmium_duration`` is reached.\n\"\"\"\n\n\nclass Workstation(UUIDModel, TitleSlugDescriptionModel):\n    \"\"\" This model holds the title and description of a workstation. \"\"\"\n\n    logo = models.ImageField(upload_to=get_logo_path)\n\n    @property\n    def latest_ready_image(self):\n        \"\"\"\n        Returns\n        -------\n            The most recent container image for this workstation\n        \"\"\"\n        return (\n            self.workstationimage_set.filter(ready=True)\n            .order_by(\"-created\")\n            .first()\n        )\n\n    def __str__(self):\n        return f\"Workstation {self.title}\"\n\n    def get_absolute_url(self):\n        return reverse(\"workstations:detail\", kwargs={\"slug\": self.slug})\n\n\nclass WorkstationImage(UUIDModel, ContainerImageModel):\n    \"\"\"\n    A ``WorkstationImage`` is a docker container image of a workstation.\n\n    Parameters\n    ----------\n    workstation\n        A ``Workstation`` can have multiple ``WorkstationImage``, that\n        represent different versions of a workstation\n    http_port\n        This container will expose a http server on this port\n    websocket_port\n        This container will expose a websocket on this port. Any relative url\n        that starts with ``/mlab4d4c4142`` will be proxied to this port.\n    initial_path\n        The initial path that users will navigate to in order to load the\n        workstation\n    \"\"\"\n\n    workstation = models.ForeignKey(Workstation, on_delete=models.CASCADE)\n    http_port = models.PositiveIntegerField(\n        default=8080, validators=[MaxValueValidator(2 ** 16 - 1)]\n    )\n    websocket_port = models.PositiveIntegerField(\n        default=4114, validators=[MaxValueValidator(2 ** 16 - 1)]\n    )\n    initial_path = models.CharField(\n        max_length=256,\n        default=\"Applications/GrandChallengeViewer/index.html\",\n        blank=True,\n        validators=[\n            RegexValidator(\n                regex=r\"^(?:[^/][^\\s]*)\\Z\",\n                message=\"This path is invalid, it must not start with a /\",\n            )\n        ],\n    )\n\n    def __str__(self):\n        return f\"Workstation Image {self.pk}\"\n\n    def get_absolute_url(self):\n        return reverse(\n            \"workstations:image-detail\",\n            kwargs={\"slug\": self.workstation.slug, \"pk\": self.pk},\n        )\n\n\nclass Session(UUIDModel):\n    \"\"\"\n    Tracks who has launched workstation images. The ``WorkstationImage`` will\n    be launched as a ``Service``. The ``Session`` is responsible for starting\n    and stopping the ``Service``.\n\n    Parameters\n    ----------\n\n    status\n        Stores what has happened with the service, is it running, errored, etc?\n    creator\n        Who created the session? This is also the only user that should be able\n        to access the launched service.\n    workstation_image\n        The container image that will be launched by this ``Session``.\n    maximum_duration\n        The maximum time that the service can be active before it is terminated\n    user_finished\n        Indicates if the user has chosen to end the session early\n    history\n        The history of this Session\n    \"\"\"\n\n    QUEUED = 0\n    STARTED = 1\n    RUNNING = 2\n    FAILED = 3\n    STOPPED = 4\n\n    # These should match the values in session.js\n    STATUS_CHOICES = (\n        (QUEUED, \"Queued\"),\n        (STARTED, \"Started\"),\n        (RUNNING, \"Running\"),\n        (FAILED, \"Failed\"),\n        (STOPPED, \"Stopped\"),\n    )\n\n    status = models.PositiveSmallIntegerField(\n        choices=STATUS_CHOICES, default=QUEUED\n    )\n    creator = models.ForeignKey(\n        settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL\n    )\n    workstation_image = models.ForeignKey(\n        WorkstationImage, on_delete=models.CASCADE\n    )\n    maximum_duration = models.DurationField(default=timedelta(minutes=10))\n    user_finished = models.BooleanField(default=False)\n    history = HistoricalRecords()\n\n    def __str__(self):\n        return f\"Session {self.pk}\"\n\n    @property\n    def task_kwargs(self) -> dict:\n        \"\"\"\n        Returns\n        -------\n            The kwargs that need to be passed to celery to get this object\n        \"\"\"\n        return {\n            \"app_label\": self._meta.app_label,\n            \"model_name\": self._meta.model_name,\n            \"pk\": self.pk,\n        }\n\n    @property\n    def hostname(self) -> str:\n        \"\"\"\n        Returns\n        -------\n            The unique hostname for this session\n        \"\"\"\n        return (\n            f\"{self.pk}.{self._meta.model_name}.{self._meta.app_label}\".lower()\n        )\n\n    @property\n    def expires_at(self) -> datetime:\n        \"\"\"\n        Returns\n        -------\n            The time when this session expires.\n        \"\"\"\n        return self.created + self.maximum_duration\n\n    @property\n    def environment(self) -> dict:\n        \"\"\"\n        Returns\n        -------\n            The environment variables that should be set on the container.\n        \"\"\"\n        env = {\n            \"GRAND_CHALLENGE_PROXY_URL_MAPPINGS\": \"\",\n            \"GRAND_CHALLENGE_QUERY_IMAGE_URL\": unquote(\n                reverse(\"api:image-detail\", kwargs={\"pk\": \"{key}\"})\n            ),\n        }\n\n        if self.creator:\n            env.update(\n                {\n                    \"GRAND_CHALLENGE_AUTHORIZATION\": f\"TOKEN {Token.objects.get_or_create(user=self.creator)[0].key}\"\n                }\n            )\n\n        if settings.DEBUG:\n            # Allow the container to communicate with the dev environment\n            env.update({\"GRAND_CHALLENGE_UNSAFE\": \"True\"})\n\n        return env\n\n    @property\n    def service(self) -> Service:\n        \"\"\"\n        Returns\n        -------\n            The service for this session, could be active or inactive.\n        \"\"\"\n        return Service(\n            job_id=self.pk,\n            job_model=f\"{self._meta.app_label}-{self._meta.model_name}\",\n            exec_image=self.workstation_image.image,\n            exec_image_sha256=self.workstation_image.image_sha256,\n        )\n\n    @property\n    def workstation_url(self) -> str:\n        \"\"\"\n        Returns\n        -------\n            The url that users will use to access the workstation instance.\n        \"\"\"\n        return urljoin(\n            self.get_absolute_url(), self.workstation_image.initial_path\n        )\n\n    def start(self) -> None:\n        \"\"\"\n        Starts the service for this session, ensuring that the\n        ``workstation_image`` is ready to be used and that\n        ``WORKSTATIONS_MAXIMUM_SESSIONS`` has not been reached.\n\n        Raises\n        ------\n        RunTimeError\n            If the service cannot be started.\n\n        Returns\n        -------\n        \"\"\"\n        try:\n            if not self.workstation_image.ready:\n                raise RuntimeError(\"Workstation image was not ready\")\n\n            if (\n                Session.objects.all()\n                .filter(status__in=[Session.RUNNING, Session.STARTED])\n                .count()\n                >= settings.WORKSTATIONS_MAXIMUM_SESSIONS\n            ):\n                raise RuntimeError(\"Too many sessions are running\")\n\n            self.service.start(\n                http_port=self.workstation_image.http_port,\n                websocket_port=self.workstation_image.websocket_port,\n                hostname=self.hostname,\n                environment=self.environment,\n            )\n            self.update_status(status=self.STARTED)\n        except RuntimeError:\n            self.update_status(status=self.FAILED)\n\n    def stop(self) -> None:\n        \"\"\"\n        Stops the service for this session, cleaning up all of the containers.\n\n        Returns\n        -------\n        \"\"\"\n        self.service.stop_and_cleanup()\n        self.update_status(status=self.STOPPED)\n\n    def update_status(self, *, status: STATUS_CHOICES) -> None:\n        \"\"\"\n        Updates the status of this session.\n\n        Parameters\n        ----------\n        status\n            The new status for this session.\n\n        Returns\n        -------\n        \"\"\"\n        self.status = status\n        self.save()\n\n    def get_absolute_url(self):\n        return reverse(\n            \"workstations:session-detail\",\n            kwargs={\n                \"slug\": self.workstation_image.workstation.slug,\n                \"pk\": self.pk,\n            },\n        )\n\n    def save(self, *args, **kwargs) -> None:\n        \"\"\"\n        Saves the session instance, starting or stopping the service if needed.\n\n        Returns\n        -------\n        \"\"\"\n        created = self._state.adding\n\n        super().save(*args, **kwargs)\n\n        if created:\n            start_service.apply_async(kwargs=self.task_kwargs)\n        elif self.user_finished and self.status != self.STOPPED:\n            stop_service.apply_async(kwargs=self.task_kwargs)\n", "path": "app/grandchallenge/workstations/models.py"}], "after_files": [{"content": "from datetime import timedelta, datetime\nfrom urllib.parse import unquote, urljoin\n\nfrom django.conf import settings\nfrom django.core.validators import MaxValueValidator, RegexValidator\nfrom django.db import models\nfrom django_extensions.db.models import TitleSlugDescriptionModel\nfrom rest_framework.authtoken.models import Token\nfrom simple_history.models import HistoricalRecords\n\nfrom grandchallenge.challenges.models import get_logo_path\nfrom grandchallenge.container_exec.backends.docker import Service\nfrom grandchallenge.container_exec.models import ContainerImageModel\nfrom grandchallenge.container_exec.tasks import start_service, stop_service\nfrom grandchallenge.core.models import UUIDModel\nfrom grandchallenge.subdomains.utils import reverse\n\n__doc__ = \"\"\"\nWorkstations are used to view, annotate and upload images to grand challenge.\nA `workstation admin` is able to upload a ``WorkstationImage``, which is a docker container image.\nA ``WorkstationImage`` expose a http and, optionally, a websocket port.\nA `workstation user` can then launch a workstation ``Session`` for a particular ``WorkstationImage``.\n\nWhen a new session is started, a new container instance of the selected ``WorkstationImage`` is lauched on the docker host.\nThe connection to the container will be proxied, and only accessible to the user that created the session.\nThe proxy will map the http and websocket connections from the user to the running instance, which is mapped by the container hostname.\nThe container instance will have the users API token set in the environment, so that it is able to interact with the grand challenge API as this user.\nThe user is able to stop the container, otherwise it will be terminated after ``maxmium_duration`` is reached.\n\"\"\"\n\n\nclass Workstation(UUIDModel, TitleSlugDescriptionModel):\n    \"\"\" This model holds the title and description of a workstation. \"\"\"\n\n    logo = models.ImageField(upload_to=get_logo_path)\n\n    @property\n    def latest_ready_image(self):\n        \"\"\"\n        Returns\n        -------\n            The most recent container image for this workstation\n        \"\"\"\n        return (\n            self.workstationimage_set.filter(ready=True)\n            .order_by(\"-created\")\n            .first()\n        )\n\n    def __str__(self):\n        return f\"Workstation {self.title}\"\n\n    def get_absolute_url(self):\n        return reverse(\"workstations:detail\", kwargs={\"slug\": self.slug})\n\n\nclass WorkstationImage(UUIDModel, ContainerImageModel):\n    \"\"\"\n    A ``WorkstationImage`` is a docker container image of a workstation.\n\n    Parameters\n    ----------\n    workstation\n        A ``Workstation`` can have multiple ``WorkstationImage``, that\n        represent different versions of a workstation\n    http_port\n        This container will expose a http server on this port\n    websocket_port\n        This container will expose a websocket on this port. Any relative url\n        that starts with ``/mlab4d4c4142`` will be proxied to this port.\n    initial_path\n        The initial path that users will navigate to in order to load the\n        workstation\n    \"\"\"\n\n    workstation = models.ForeignKey(Workstation, on_delete=models.CASCADE)\n    http_port = models.PositiveIntegerField(\n        default=8080, validators=[MaxValueValidator(2 ** 16 - 1)]\n    )\n    websocket_port = models.PositiveIntegerField(\n        default=4114, validators=[MaxValueValidator(2 ** 16 - 1)]\n    )\n    initial_path = models.CharField(\n        max_length=256,\n        default=\"Applications/GrandChallengeViewer/index.html\",\n        blank=True,\n        validators=[\n            RegexValidator(\n                regex=r\"^(?:[^/][^\\s]*)\\Z\",\n                message=\"This path is invalid, it must not start with a /\",\n            )\n        ],\n    )\n\n    def __str__(self):\n        return f\"Workstation Image {self.pk}\"\n\n    def get_absolute_url(self):\n        return reverse(\n            \"workstations:image-detail\",\n            kwargs={\"slug\": self.workstation.slug, \"pk\": self.pk},\n        )\n\n\nclass Session(UUIDModel):\n    \"\"\"\n    Tracks who has launched workstation images. The ``WorkstationImage`` will\n    be launched as a ``Service``. The ``Session`` is responsible for starting\n    and stopping the ``Service``.\n\n    Parameters\n    ----------\n\n    status\n        Stores what has happened with the service, is it running, errored, etc?\n    creator\n        Who created the session? This is also the only user that should be able\n        to access the launched service.\n    workstation_image\n        The container image that will be launched by this ``Session``.\n    maximum_duration\n        The maximum time that the service can be active before it is terminated\n    user_finished\n        Indicates if the user has chosen to end the session early\n    history\n        The history of this Session\n    \"\"\"\n\n    QUEUED = 0\n    STARTED = 1\n    RUNNING = 2\n    FAILED = 3\n    STOPPED = 4\n\n    # These should match the values in session.js\n    STATUS_CHOICES = (\n        (QUEUED, \"Queued\"),\n        (STARTED, \"Started\"),\n        (RUNNING, \"Running\"),\n        (FAILED, \"Failed\"),\n        (STOPPED, \"Stopped\"),\n    )\n\n    status = models.PositiveSmallIntegerField(\n        choices=STATUS_CHOICES, default=QUEUED\n    )\n    creator = models.ForeignKey(\n        settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL\n    )\n    workstation_image = models.ForeignKey(\n        WorkstationImage, on_delete=models.CASCADE\n    )\n    maximum_duration = models.DurationField(default=timedelta(minutes=10))\n    user_finished = models.BooleanField(default=False)\n    history = HistoricalRecords()\n\n    def __str__(self):\n        return f\"Session {self.pk}\"\n\n    @property\n    def task_kwargs(self) -> dict:\n        \"\"\"\n        Returns\n        -------\n            The kwargs that need to be passed to celery to get this object\n        \"\"\"\n        return {\n            \"app_label\": self._meta.app_label,\n            \"model_name\": self._meta.model_name,\n            \"pk\": self.pk,\n        }\n\n    @property\n    def hostname(self) -> str:\n        \"\"\"\n        Returns\n        -------\n            The unique hostname for this session\n        \"\"\"\n        return (\n            f\"{self.pk}.{self._meta.model_name}.{self._meta.app_label}\".lower()\n        )\n\n    @property\n    def expires_at(self) -> datetime:\n        \"\"\"\n        Returns\n        -------\n            The time when this session expires.\n        \"\"\"\n        return self.created + self.maximum_duration\n\n    @property\n    def environment(self) -> dict:\n        \"\"\"\n        Returns\n        -------\n            The environment variables that should be set on the container.\n        \"\"\"\n        env = {\"GRAND_CHALLENGE_API_ROOT\": unquote(reverse(\"api:api-root\"))}\n\n        if self.creator:\n            env.update(\n                {\n                    \"GRAND_CHALLENGE_AUTHORIZATION\": f\"TOKEN {Token.objects.get_or_create(user=self.creator)[0].key}\"\n                }\n            )\n\n        if settings.DEBUG:\n            # Allow the container to communicate with the dev environment\n            env.update({\"GRAND_CHALLENGE_UNSAFE\": \"True\"})\n\n        return env\n\n    @property\n    def service(self) -> Service:\n        \"\"\"\n        Returns\n        -------\n            The service for this session, could be active or inactive.\n        \"\"\"\n        return Service(\n            job_id=self.pk,\n            job_model=f\"{self._meta.app_label}-{self._meta.model_name}\",\n            exec_image=self.workstation_image.image,\n            exec_image_sha256=self.workstation_image.image_sha256,\n        )\n\n    @property\n    def workstation_url(self) -> str:\n        \"\"\"\n        Returns\n        -------\n            The url that users will use to access the workstation instance.\n        \"\"\"\n        return urljoin(\n            self.get_absolute_url(), self.workstation_image.initial_path\n        )\n\n    def start(self) -> None:\n        \"\"\"\n        Starts the service for this session, ensuring that the\n        ``workstation_image`` is ready to be used and that\n        ``WORKSTATIONS_MAXIMUM_SESSIONS`` has not been reached.\n\n        Raises\n        ------\n        RunTimeError\n            If the service cannot be started.\n\n        Returns\n        -------\n        \"\"\"\n        try:\n            if not self.workstation_image.ready:\n                raise RuntimeError(\"Workstation image was not ready\")\n\n            if (\n                Session.objects.all()\n                .filter(status__in=[Session.RUNNING, Session.STARTED])\n                .count()\n                >= settings.WORKSTATIONS_MAXIMUM_SESSIONS\n            ):\n                raise RuntimeError(\"Too many sessions are running\")\n\n            self.service.start(\n                http_port=self.workstation_image.http_port,\n                websocket_port=self.workstation_image.websocket_port,\n                hostname=self.hostname,\n                environment=self.environment,\n            )\n            self.update_status(status=self.STARTED)\n        except RuntimeError:\n            self.update_status(status=self.FAILED)\n\n    def stop(self) -> None:\n        \"\"\"\n        Stops the service for this session, cleaning up all of the containers.\n\n        Returns\n        -------\n        \"\"\"\n        self.service.stop_and_cleanup()\n        self.update_status(status=self.STOPPED)\n\n    def update_status(self, *, status: STATUS_CHOICES) -> None:\n        \"\"\"\n        Updates the status of this session.\n\n        Parameters\n        ----------\n        status\n            The new status for this session.\n\n        Returns\n        -------\n        \"\"\"\n        self.status = status\n        self.save()\n\n    def get_absolute_url(self):\n        return reverse(\n            \"workstations:session-detail\",\n            kwargs={\n                \"slug\": self.workstation_image.workstation.slug,\n                \"pk\": self.pk,\n            },\n        )\n\n    def save(self, *args, **kwargs) -> None:\n        \"\"\"\n        Saves the session instance, starting or stopping the service if needed.\n\n        Returns\n        -------\n        \"\"\"\n        created = self._state.adding\n\n        super().save(*args, **kwargs)\n\n        if created:\n            start_service.apply_async(kwargs=self.task_kwargs)\n        elif self.user_finished and self.status != self.STOPPED:\n            stop_service.apply_async(kwargs=self.task_kwargs)\n", "path": "app/grandchallenge/workstations/models.py"}]}