From 363d00d2bd77a304104182a0a1ec69070e136290 Mon Sep 17 00:00:00 2001 From: Juan Puerto <=> Date: Wed, 29 Jan 2025 15:29:15 -0500 Subject: [PATCH] Reapply "Merge branch 'development' into phillips/custom_parameters" This reverts commit bc7cc0cd5a76228f8e4db689692397a2ccac1ada. --- requirements/requirements.txt | 2 +- src/user_workspaces_server/admin.py | 10 +- .../migrations/0014_sharedworkspacemapping.py | 44 +++ ...0015_sharedworkspacemapping_is_accepted.py | 18 ++ ...dworkspacemapping_last_resource_options.py | 18 ++ ...workspacemapping_datetime_share_created.py | 22 ++ src/user_workspaces_server/models.py | 28 ++ src/user_workspaces_server/serializers.py | 36 +++ src/user_workspaces_server/tasks.py | 55 ++++ .../email_templates/example_share_email.txt | 9 + src/user_workspaces_server/urls.py | 30 ++ .../views/shared_workspace_view.py | 265 ++++++++++++++++++ src/user_workspaces_server/views/user_view.py | 36 +++ .../user_workspaces_server_token_view.py | 4 + .../views/workspace_view.py | 22 ++ 15 files changed, 597 insertions(+), 2 deletions(-) create mode 100644 src/user_workspaces_server/migrations/0014_sharedworkspacemapping.py create mode 100644 src/user_workspaces_server/migrations/0015_sharedworkspacemapping_is_accepted.py create mode 100644 src/user_workspaces_server/migrations/0016_rename_last_params_sharedworkspacemapping_last_resource_options.py create mode 100644 src/user_workspaces_server/migrations/0017_sharedworkspacemapping_datetime_share_created.py create mode 100644 src/user_workspaces_server/serializers.py create mode 100644 src/user_workspaces_server/templates/email_templates/example_share_email.txt create mode 100644 src/user_workspaces_server/views/shared_workspace_view.py create mode 100644 src/user_workspaces_server/views/user_view.py diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 5a99e8b..beab701 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -21,7 +21,7 @@ Django==5.1.3 django-cors-headers==3.13.0 django-picklefield==3.1 django-q2==1.7.3 -djangorestframework==3.14.0 +djangorestframework==3.15.2 Flask==2.2.2 globus-sdk==3.12.0 h11==0.14.0 diff --git a/src/user_workspaces_server/admin.py b/src/user_workspaces_server/admin.py index e715144..492993e 100644 --- a/src/user_workspaces_server/admin.py +++ b/src/user_workspaces_server/admin.py @@ -3,4 +3,12 @@ from user_workspaces_server import models # Register your models here. -admin.site.register([models.ExternalUserMapping, models.UserQuota, models.Workspace, models.Job]) +admin.site.register( + [ + models.ExternalUserMapping, + models.UserQuota, + models.Workspace, + models.Job, + models.SharedWorkspaceMapping, + ] +) diff --git a/src/user_workspaces_server/migrations/0014_sharedworkspacemapping.py b/src/user_workspaces_server/migrations/0014_sharedworkspacemapping.py new file mode 100644 index 0000000..b38dfd9 --- /dev/null +++ b/src/user_workspaces_server/migrations/0014_sharedworkspacemapping.py @@ -0,0 +1,44 @@ +# Generated by Django 4.1.2 on 2025-01-06 19:36 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("user_workspaces_server", "0013_alter_workspace_disk_space"), + ] + + operations = [ + migrations.CreateModel( + name="SharedWorkspaceMapping", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("last_params", models.JSONField(blank=True, null=True)), + ("last_job_type", models.CharField(max_length=64, null=True)), + ( + "original_workspace_id", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="original_workspace_set", + to="user_workspaces_server.workspace", + ), + ), + ( + "shared_workspace_id", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="shared_workspace_set", + to="user_workspaces_server.workspace", + ), + ), + ], + ), + ] diff --git a/src/user_workspaces_server/migrations/0015_sharedworkspacemapping_is_accepted.py b/src/user_workspaces_server/migrations/0015_sharedworkspacemapping_is_accepted.py new file mode 100644 index 0000000..5e49e1b --- /dev/null +++ b/src/user_workspaces_server/migrations/0015_sharedworkspacemapping_is_accepted.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.2 on 2025-01-07 18:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("user_workspaces_server", "0014_sharedworkspacemapping"), + ] + + operations = [ + migrations.AddField( + model_name="sharedworkspacemapping", + name="is_accepted", + field=models.BooleanField(default=False), + ), + ] diff --git a/src/user_workspaces_server/migrations/0016_rename_last_params_sharedworkspacemapping_last_resource_options.py b/src/user_workspaces_server/migrations/0016_rename_last_params_sharedworkspacemapping_last_resource_options.py new file mode 100644 index 0000000..b04fa0e --- /dev/null +++ b/src/user_workspaces_server/migrations/0016_rename_last_params_sharedworkspacemapping_last_resource_options.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.17 on 2025-01-13 20:14 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("user_workspaces_server", "0015_sharedworkspacemapping_is_accepted"), + ] + + operations = [ + migrations.RenameField( + model_name="sharedworkspacemapping", + old_name="last_params", + new_name="last_resource_options", + ), + ] diff --git a/src/user_workspaces_server/migrations/0017_sharedworkspacemapping_datetime_share_created.py b/src/user_workspaces_server/migrations/0017_sharedworkspacemapping_datetime_share_created.py new file mode 100644 index 0000000..50e6b70 --- /dev/null +++ b/src/user_workspaces_server/migrations/0017_sharedworkspacemapping_datetime_share_created.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.17 on 2025-01-14 15:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "user_workspaces_server", + "0016_rename_last_params_sharedworkspacemapping_last_resource_options", + ), + ] + + operations = [ + migrations.AddField( + model_name="sharedworkspacemapping", + name="datetime_share_created", + field=models.DateTimeField(null=True), + preserve_default=False, + ), + ] diff --git a/src/user_workspaces_server/models.py b/src/user_workspaces_server/models.py index a49548e..6e2e2e4 100644 --- a/src/user_workspaces_server/models.py +++ b/src/user_workspaces_server/models.py @@ -107,3 +107,31 @@ def __str__(self): f"{self.id}: {self.user_id.username if self.user_id else 'User Missing'} -" f" {self.user_authentication_name}" ) + + +class SharedWorkspaceMapping(models.Model): + original_workspace_id = models.ForeignKey( + Workspace, + on_delete=models.SET_NULL, + null=True, + related_name="original_workspace_set", + ) + shared_workspace_id = models.ForeignKey( + Workspace, on_delete=models.CASCADE, related_name="shared_workspace_set" + ) + last_resource_options = models.JSONField(blank=True, null=True) + last_job_type = models.CharField(max_length=64, null=True) + is_accepted = models.BooleanField(default=False) + datetime_share_created = models.DateTimeField(null=True) + + @staticmethod + def get_query_param_fields(): + return ["is_accepted"] + + @staticmethod + def get_dict_fields(): + return [ + "is_accepted", + "last_resource_options", + "last_job_type", + ] diff --git a/src/user_workspaces_server/serializers.py b/src/user_workspaces_server/serializers.py new file mode 100644 index 0000000..aa402d7 --- /dev/null +++ b/src/user_workspaces_server/serializers.py @@ -0,0 +1,36 @@ +from django.contrib.auth.models import User +from rest_framework import serializers + +from user_workspaces_server.models import SharedWorkspaceMapping, Workspace + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ["username", "first_name", "last_name", "email"] + + +class WorkspaceSerializer(serializers.ModelSerializer): + user_id = UserSerializer(read_only=True) + + class Meta: + model = Workspace + fields = Workspace.get_dict_fields() + fields.append("user_id") + + def __init__(self, *args, **kwargs): + workspace_type = kwargs.pop("workspace_type", None) + super().__init__(*args, **kwargs) + + if workspace_type == "shared_workspace": + for field_name in set(self.fields) - {"id", "user_id", "name", "description"}: + self.fields.pop(field_name) + + +class SharedWorkspaceMappingSerializer(serializers.ModelSerializer): + original_workspace_id = WorkspaceSerializer(read_only=True, workspace_type="shared_workspace") + shared_workspace_id = WorkspaceSerializer(read_only=True, workspace_type="shared_workspace") + + class Meta: + model = SharedWorkspaceMapping + exclude = ["id"] diff --git a/src/user_workspaces_server/tasks.py b/src/user_workspaces_server/tasks.py index acd26c4..231049c 100644 --- a/src/user_workspaces_server/tasks.py +++ b/src/user_workspaces_server/tasks.py @@ -1,12 +1,14 @@ import datetime import logging import os +import shutil from asgiref.sync import async_to_sync from channels.layers import get_channel_layer from django.apps import apps from django.conf import settings from django.db.models import Sum +from django.template.loader import render_to_string from django_q.brokers import get_broker from django_q.tasks import async_task @@ -285,3 +287,56 @@ def update_user_quota_core_hours(user_quota_id): workspace_id__user_id=user_quota.user_id ).aggregate(Sum("core_hours"))["core_hours__sum"] user_quota.save() + + +def initialize_shared_workspace(shared_workspace_mapping_id: int): + shared_workspace_mapping = models.SharedWorkspaceMapping.objects.get( + pk=shared_workspace_mapping_id + ) + original_workspace = shared_workspace_mapping.original_workspace_id + shared_workspace = shared_workspace_mapping.shared_workspace_id + + main_storage = apps.get_app_config("user_workspaces_server").main_storage + external_user_mapping = main_storage.storage_user_authentication.has_permission( + shared_workspace.user_id + ) + + # Set the shared_workspace file path + shared_workspace.file_path = os.path.join( + external_user_mapping.external_username, str(shared_workspace.pk) + ) + + try: + # Copy non . directories + shutil.copytree( + os.path.join(main_storage.root_dir, original_workspace.file_path), + os.path.join(main_storage.root_dir, shared_workspace.file_path), + ignore=shutil.ignore_patterns(".*"), + symlinks=True, + ) + main_storage.set_ownership( + shared_workspace.file_path, external_user_mapping, recursive=True + ) + except Exception as e: + logger.exception(f"Copying files for {shared_workspace_mapping} failed: {e}") + + async_update_workspace(shared_workspace.pk) + + message = render_to_string( + "email_templates/share_email.txt", + context={ + "sharer": original_workspace.user_id, + "receiver": shared_workspace.user_id, + "mapping_details": shared_workspace_mapping, + "original_workspace": original_workspace, + }, + ) + async_task( + "django.core.mail.send_mail", + "Invitation to Share a Workspace", + message, + None, + [shared_workspace.user_id.email], + ) + + shared_workspace.save() diff --git a/src/user_workspaces_server/templates/email_templates/example_share_email.txt b/src/user_workspaces_server/templates/email_templates/example_share_email.txt new file mode 100644 index 0000000..3ccd06a --- /dev/null +++ b/src/user_workspaces_server/templates/email_templates/example_share_email.txt @@ -0,0 +1,9 @@ +Dear {{ receiver.first_name }} {{ receiver.last_name }}, + +{{ sharer.first_name }} {{ sharer.last_name }} would like to share their workspace with you. + +This workspace is called {{ original_workspace.name }} and is described as {{ original_workspace.description }}. + +Best regards, + +Data Portal Team \ No newline at end of file diff --git a/src/user_workspaces_server/urls.py b/src/user_workspaces_server/urls.py index 9849aa7..a1f2a8e 100644 --- a/src/user_workspaces_server/urls.py +++ b/src/user_workspaces_server/urls.py @@ -23,7 +23,9 @@ job_type_view, job_view, passthrough_view, + shared_workspace_view, status_view, + user_view, user_workspaces_server_token_view, workspace_view, ) @@ -77,6 +79,32 @@ ), ] +user_view_patterns = [ + path( + "", + user_view.UserView.as_view(), + name="users", + ) +] + +shared_workspace_view_patterns = [ + path( + "", + shared_workspace_view.SharedWorkspaceView.as_view(), + name="shared_workspaces", + ), + path( + "/", + shared_workspace_view.SharedWorkspaceView.as_view(), + name="shared_workspaces_with_id", + ), + path( + "//", + shared_workspace_view.SharedWorkspaceView.as_view(), + name="shared_workspaces_put_type", + ), +] + urlpatterns = [ path("tokens/", include(token_view_patterns)), path("workspaces/", include(workspace_view_patterns)), @@ -84,6 +112,8 @@ path("job_types/", include(job_type_view_patterns)), path("passthrough/", include(passthrough_view_patterns)), path("parameters/", include(parameter_view_patterns)), + path("users/", include(user_view_patterns)), + path("shared_workspaces/", include(shared_workspace_view_patterns)), path("status/", status_view.StatusView.as_view(), name="status"), ] diff --git a/src/user_workspaces_server/views/shared_workspace_view.py b/src/user_workspaces_server/views/shared_workspace_view.py new file mode 100644 index 0000000..02621d5 --- /dev/null +++ b/src/user_workspaces_server/views/shared_workspace_view.py @@ -0,0 +1,265 @@ +import json +import logging +from datetime import datetime + +from django.apps import apps +from django.contrib.auth.models import User +from django.http import JsonResponse +from django_q.tasks import async_task +from rest_framework.exceptions import ( + APIException, + NotFound, + ParseError, + PermissionDenied, +) +from rest_framework.permissions import IsAuthenticated +from rest_framework.views import APIView + +from user_workspaces_server import models, serializers +from user_workspaces_server.exceptions import WorkspaceClientException + +logger = logging.getLogger(__name__) + + +class SharedWorkspaceView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request, shared_workspace_id=None): + # For a given user we need to return: + # - The shared workspaces for which they've sent requests + # - The shared workspaces for which they've received requests + # We need to filter based on whether they've been accepted or not + # We need to allow for a single shared workspace to be returned + + original_workspaces = models.SharedWorkspaceMapping.objects.filter( + original_workspace_id__user_id=request.user + ) + + shared_workspaces = models.SharedWorkspaceMapping.objects.filter( + shared_workspace_id__user_id=request.user + ) + + if shared_workspace_id: + shared_workspaces = shared_workspaces.filter( + shared_workspace_id__pk=shared_workspace_id + ) + original_workspaces = original_workspaces.filter( + shared_workspace_id__pk=shared_workspace_id + ) + elif params := request.GET: + for key in set(params.keys()).intersection( + models.SharedWorkspaceMapping.get_query_param_fields() + ): + original_workspaces = original_workspaces.filter(**{key: params[key]}) + shared_workspaces = shared_workspaces.filter(**{key: params[key]}) + + original_workspaces = serializers.SharedWorkspaceMappingSerializer( + original_workspaces, many=True + ).data + shared_workspaces = serializers.SharedWorkspaceMappingSerializer( + shared_workspaces, many=True + ).data + + response = { + "message": "Successful.", + "success": True, + "data": {"original_workspaces": [], "shared_workspaces": []}, + } + + if original_workspaces or shared_workspaces: + response["data"]["original_workspaces"] = original_workspaces + response["data"]["shared_workspaces"] = shared_workspaces + else: + response["message"] = "Shared workspace matching given parameters could not be found." + + return JsonResponse(response) + + def post(self, request): + # Begin Validity check section + # Basic validity checks + try: + body = json.loads(request.body) + except Exception as e: + raise ParseError(f"Invalid JSON: {str(e)}") + + if "shared_user_ids" not in body or "original_workspace_id" not in body: + raise ParseError("Missing required fields.") + + # Check workspace_id is valid/owned by current user + try: + workspace = models.Workspace.objects.get( + user_id=request.user, id=body["original_workspace_id"] + ) + except Exception: + raise NotFound(f"Workspace {body['original_workspace_id']} not found for user.") + + shared_user_ids = body["shared_user_ids"] + + # Check user_ids_to_share are valid + if not isinstance(shared_user_ids, list): + raise ParseError("shared_user_ids is not a list.") + elif len(shared_users := User.objects.filter(pk__in=shared_user_ids)) != len( + shared_user_ids + ): + raise ParseError("Invalid user id provided.") + + for user in shared_users: + # Check whether user has permission + main_storage = apps.get_app_config("user_workspaces_server").main_storage + if not main_storage.storage_user_authentication.has_permission(user): + raise WorkspaceClientException( + f"User {user.first_name} {user.last_name} does not have permission on the file system." + ) + + # Begin DAO inserts + # Get latest job for last_params and last_job_type + try: + latest_job = models.Job.objects.filter(workspace_id=workspace).latest( + "datetime_created" + ) + except Exception: + latest_job = None + + # Same set of data will be used across all shared_workspace entries + workspace_data = { + "name": workspace.name, + "description": workspace.description, + "workspace_details": workspace.workspace_details, + "default_job_type": workspace.default_job_type, + "datetime_created": datetime.now(), + } + + shared_workspace_data = { + "original_workspace_id": workspace, + "shared_workspace_id": None, + "last_resource_options": {} if latest_job is None else latest_job.resource_options, + "last_job_type": "" if latest_job is None else latest_job.job_type, + "is_accepted": False, + } + + shared_workspaces_created = [] + for user in shared_users: + # Prepare workspace model creation + workspace_data_copy = workspace_data.copy() + workspace_data_copy["user_id"] = user + new_workspace = models.Workspace.objects.create(**workspace_data_copy) + + # Create shared workspace mapping + shared_workspace_data_copy = shared_workspace_data.copy() + shared_workspace_data_copy["shared_workspace_id"] = new_workspace + shared_workspace_data_copy["datetime_share_created"] = datetime.now() + shared_workspace = models.SharedWorkspaceMapping.objects.create( + **shared_workspace_data_copy + ) + shared_workspaces_created.append(shared_workspace) + + for shared_workspace_created in shared_workspaces_created: + logger.info( + f"Shared workspace created, sending to initialize: {shared_workspace_created}" + ) + async_task( + "user_workspaces_server.tasks.initialize_shared_workspace", + shared_workspace_created.pk, + cluster="long", + ) + + shared_workspaces_created = serializers.SharedWorkspaceMappingSerializer( + shared_workspaces_created, many=True + ).data + + return JsonResponse( + { + "message": "Successful.", + "success": True, + "data": { + "shared_workspaces": shared_workspaces_created, + }, + } + ) + + def put(self, request, shared_workspace_id, put_type): + # Basic validation checks + try: + shared_workspace_mapping = models.SharedWorkspaceMapping.objects.get( + shared_workspace_id=shared_workspace_id, + shared_workspace_id__user_id=request.user, + ) + except Exception: + raise NotFound(f"Shared workspace {shared_workspace_id} not found.") + + if put_type == "accept": + # Set is_accepted to true for shared_workspace_id + shared_workspace_mapping.is_accepted = True + shared_workspace_mapping.save() + + # Update the created timestamp for the workspace once they accept the request + shared_workspace = shared_workspace_mapping.shared_workspace_id + shared_workspace.datetime_created = datetime.now() + shared_workspace.save() + else: + raise NotFound(f"Put type {put_type} not supported.") + + return JsonResponse({"message": "Successful.", "success": True}) + + def delete(self, request, shared_workspace_id): + # Basic validation checks + + # Check that the workspace exists + try: + shared_workspace_mapping = models.SharedWorkspaceMapping.objects.get( + shared_workspace_id__pk=shared_workspace_id + ) + except models.SharedWorkspaceMapping.DoesNotExist: + raise NotFound(f"Shared workspace {shared_workspace_id} not found.") + + # Check ownership of either original workspace or shared workspace + if request.user not in [ + shared_workspace_mapping.shared_workspace_id.user_id, + shared_workspace_mapping.original_workspace_id.user_id, + ]: + raise PermissionDenied( + f"User does not have permissions for shared workspace {shared_workspace_id}" + ) + + # Check that the workspace hasn't been accepted + if shared_workspace_mapping.is_accepted: + raise WorkspaceClientException( + f"Shared workspace {shared_workspace_id} has been accepted and cannot be deleted." + ) + + shared_workspace = shared_workspace_mapping.shared_workspace_id + + if models.Job.objects.filter( + workspace_id=shared_workspace, status__in=["pending", "running"] + ).exists(): + raise WorkspaceClientException( + "Cannot delete workspace, jobs are running for this workspace." + ) + + main_storage = apps.get_app_config("user_workspaces_server").main_storage + external_user_mapping = main_storage.storage_user_authentication.has_permission( + request.user + ) + + if not external_user_mapping: + raise WorkspaceClientException( + "User could not be found/created on main storage system." + ) + + if not main_storage.is_valid_path(shared_workspace.file_path): + logger.error(f"Workspace {shared_workspace.pk} deletion failed due to invalid path") + shared_workspace.status = models.Workspace.Status.ERROR + shared_workspace.save() + raise APIException( + "Please contact a system administrator there is a failure with " + "the workspace directory that will not allow for this workspace to be deleted." + ) + + shared_workspace.status = models.Workspace.Status.DELETING + shared_workspace.save() + + async_task( + "user_workspaces_server.tasks.delete_workspace", shared_workspace.pk, cluster="long" + ) + + return JsonResponse({"message": "Successful.", "success": True}) diff --git a/src/user_workspaces_server/views/user_view.py b/src/user_workspaces_server/views/user_view.py new file mode 100644 index 0000000..15176ab --- /dev/null +++ b/src/user_workspaces_server/views/user_view.py @@ -0,0 +1,36 @@ +import logging + +from django.contrib.auth.models import User +from django.db.models import Q, Value +from django.db.models.functions import Concat +from django.http import JsonResponse +from rest_framework.authtoken.views import APIView +from rest_framework.permissions import IsAuthenticated + +logger = logging.getLogger(__name__) + + +class UserView(APIView): + permission_classes = [IsAuthenticated] + filter_user_fields = ["first_name", "last_name", "username", "email"] + return_user_fields = ["id", "first_name", "last_name", "username", "email"] + + def get(self, request): + users = User.objects.all() + + if request.GET and "search" in request.GET: + search = request.GET["search"] + users = users.annotate( + first_last=Concat("first_name", Value(" "), "last_name") + ).filter(Q(first_last__icontains=search) | Q(email__icontains=search)) + + users = list(users.all().values(*self.return_user_fields)) + + response = {"message": "Successful.", "success": True, "data": {"users": []}} + + if users: + response["data"]["users"] = users + else: + response["message"] = "Users matching given parameters could not be found." + + return JsonResponse(response) diff --git a/src/user_workspaces_server/views/user_workspaces_server_token_view.py b/src/user_workspaces_server/views/user_workspaces_server_token_view.py index 9aade2d..43e5adc 100644 --- a/src/user_workspaces_server/views/user_workspaces_server_token_view.py +++ b/src/user_workspaces_server/views/user_workspaces_server_token_view.py @@ -29,6 +29,10 @@ def post(self, request, *args, **kwargs): "token": token.key, } ) + # TODO: Start an async routine to confirm "main storage" user + # external_user_mapping = main_storage.storage_user_authentication.has_permission( + # request.user + # ) elif isinstance(api_user, Response): result = api_user else: diff --git a/src/user_workspaces_server/views/workspace_view.py b/src/user_workspaces_server/views/workspace_view.py index c8e3d18..cc6a8f0 100644 --- a/src/user_workspaces_server/views/workspace_view.py +++ b/src/user_workspaces_server/views/workspace_view.py @@ -165,6 +165,17 @@ def put(self, request, workspace_id, put_type=None): except models.Workspace.DoesNotExist: raise NotFound(f"Workspace {workspace_id} not found for user.") + try: + shared_workspace = models.SharedWorkspaceMapping.objects.get( + shared_workspace_id=workspace + ) + if not shared_workspace.is_accepted: + raise WorkspaceClientException( + f"Workspace {workspace_id} is a shared workspace and has not been accepted." + ) + except models.SharedWorkspaceMapping.DoesNotExist: + pass + if not put_type: try: body = json.loads(request.body) @@ -348,6 +359,17 @@ def delete(self, request, workspace_id): except models.Workspace.DoesNotExist: raise NotFound(f"Workspace {workspace_id} not found for user.") + try: + shared_workspace = models.SharedWorkspaceMapping.objects.get( + shared_workspace_id=workspace + ) + if not shared_workspace.is_accepted: + raise WorkspaceClientException( + f"Workspace {workspace_id} is a shared workspace and has not been accepted." + ) + except models.SharedWorkspaceMapping.DoesNotExist: + pass + if models.Job.objects.filter( workspace_id=workspace, status__in=["pending", "running"] ).exists():