Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 4.2.1 on 2025-10-15 06:30

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("prompt_studio_core_v2", "0003_alter_customtool_summarize_llm_adapter"),
]

operations = [
migrations.AddField(
model_name="customtool",
name="shared_to_org",
field=models.BooleanField(
db_comment="Flag to share this custom tool with all users in the organization",
default=False,
),
),
]
12 changes: 11 additions & 1 deletion backend/prompt_studio/prompt_studio_core_v2/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ class CustomToolModelManager(DefaultOrganizationManagerMixin, models.Manager):
def for_user(self, user: User) -> QuerySet[Any]:
return (
self.get_queryset()
.filter(models.Q(created_by=user) | models.Q(shared_users=user))
.filter(
models.Q(created_by=user)
| models.Q(shared_users=user)
| models.Q(shared_to_org=True)
)
.distinct("tool_id")
)

Expand Down Expand Up @@ -145,6 +149,12 @@ class CustomTool(DefaultOrganizationMixin, BaseModel):
# This will introduce intermediary table which relates both the models.
shared_users = models.ManyToManyField(User, related_name="shared_custom_tools")

# Field to enable organization-level sharing
shared_to_org = models.BooleanField(
default=False,
db_comment="Flag to share this custom tool with all users in the organization",
)

objects = CustomToolModelManager()

def delete(self, organization_id=None, *args, **kwargs):
Expand Down
1 change: 1 addition & 0 deletions backend/prompt_studio/prompt_studio_core_v2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ class Meta:
"tool_name",
"created_by",
"shared_users",
"shared_to_org",
)


Expand Down
45 changes: 42 additions & 3 deletions backend/prompt_studio/prompt_studio_core_v2/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from django.http import HttpRequest, HttpResponse
from file_management.constants import FileInformationKey as FileKey
from file_management.exceptions import FileNotFound
from permissions.permission import IsOwner, IsOwnerOrSharedUser
from permissions.permission import IsOwner, IsOwnerOrSharedUserOrSharedToOrg
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.request import Request
Expand Down Expand Up @@ -94,7 +94,7 @@ def get_permissions(self) -> list[Any]:
if self.action == "destroy":
return [IsOwner()]

return [IsOwnerOrSharedUser()]
return [IsOwnerOrSharedUserOrSharedToOrg()]

def get_queryset(self) -> QuerySet | None:
return CustomTool.objects.for_user(self.request.user)
Expand Down Expand Up @@ -153,7 +153,46 @@ def destroy(
def partial_update(
self, request: Request, *args: tuple[Any], **kwargs: dict[str, Any]
) -> Response:
return super().partial_update(request, *args, **kwargs)
# Store current shared users before update for email notifications
custom_tool = self.get_object()
current_shared_users = set(custom_tool.shared_users.all())

# Perform the update
response = super().partial_update(request, *args, **kwargs)

# Send email notifications to newly shared users
if response.status_code == 200 and "shared_users" in request.data:
from plugins.notification.constants import ResourceType
from plugins.notification.sharing_notification import (
SharingNotificationService,
)

# Refresh the object to get updated shared_users
custom_tool.refresh_from_db()
updated_shared_users = set(custom_tool.shared_users.all())

# Find newly added users (not previously shared)
newly_shared_users = updated_shared_users - current_shared_users

if newly_shared_users:
notification_service = SharingNotificationService()
try:
notification_service.send_sharing_notification(
resource_type=ResourceType.TEXT_EXTRACTOR.value,
resource_name=custom_tool.tool_name,
resource_id=str(custom_tool.tool_id),
shared_by=request.user,
shared_to=list(newly_shared_users),
resource_instance=custom_tool,
)
except Exception as e:
# Log error but don't fail the request
logger.exception(
f"Failed to send sharing notification for "
f"custom tool {custom_tool.tool_id}: {str(e)}"
)

return response

@action(detail=True, methods=["get"])
def get_select_choices(self, request: HttpRequest) -> Response:
Expand Down
12 changes: 10 additions & 2 deletions frontend/src/components/custom-tools/list-of-tools/ListOfTools.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -313,18 +313,25 @@ function ListOfTools() {
});
};

const onShare = (userIds, adapter) => {
const onShare = (userIds, adapter, shareWithEveryone) => {
const requestOptions = {
method: "PATCH",
url: `/api/v1/unstract/${sessionDetails?.orgId}/prompt-studio/${adapter?.tool_id}`,
headers: {
"X-CSRFToken": sessionDetails?.csrfToken,
},
data: { shared_users: userIds },
data: {
shared_users: userIds,
shared_to_org: shareWithEveryone || false,
},
};
axiosPrivate(requestOptions)
.then((response) => {
setOpenSharePermissionModal(false);
setAlertDetails({
type: "success",
content: "Sharing settings updated successfully",
});
})
.catch((err) => {
setAlertDetails(handleException(err, "Failed to load"));
Expand Down Expand Up @@ -384,6 +391,7 @@ function ListOfTools() {
loading={isShareLoading}
allUsers={allUserList}
onApply={onShare}
isSharableToOrg={true}
/>
</>
);
Expand Down