diff --git a/backend/prompt_studio/prompt_studio_core_v2/migrations/0004_add_shared_to_org_to_custom_tool.py b/backend/prompt_studio/prompt_studio_core_v2/migrations/0004_add_shared_to_org_to_custom_tool.py new file mode 100644 index 0000000000..32e8053bdc --- /dev/null +++ b/backend/prompt_studio/prompt_studio_core_v2/migrations/0004_add_shared_to_org_to_custom_tool.py @@ -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, + ), + ), + ] diff --git a/backend/prompt_studio/prompt_studio_core_v2/models.py b/backend/prompt_studio/prompt_studio_core_v2/models.py index 45e3621415..1abb5df0bd 100644 --- a/backend/prompt_studio/prompt_studio_core_v2/models.py +++ b/backend/prompt_studio/prompt_studio_core_v2/models.py @@ -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") ) @@ -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): diff --git a/backend/prompt_studio/prompt_studio_core_v2/serializers.py b/backend/prompt_studio/prompt_studio_core_v2/serializers.py index c026c5e758..d371936f1f 100644 --- a/backend/prompt_studio/prompt_studio_core_v2/serializers.py +++ b/backend/prompt_studio/prompt_studio_core_v2/serializers.py @@ -185,6 +185,7 @@ class Meta: "tool_name", "created_by", "shared_users", + "shared_to_org", ) diff --git a/backend/prompt_studio/prompt_studio_core_v2/views.py b/backend/prompt_studio/prompt_studio_core_v2/views.py index 501978f20a..b6495404b2 100644 --- a/backend/prompt_studio/prompt_studio_core_v2/views.py +++ b/backend/prompt_studio/prompt_studio_core_v2/views.py @@ -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 @@ -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) @@ -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: diff --git a/frontend/src/components/custom-tools/list-of-tools/ListOfTools.jsx b/frontend/src/components/custom-tools/list-of-tools/ListOfTools.jsx index 48fcfb1801..ccdc8f1050 100644 --- a/frontend/src/components/custom-tools/list-of-tools/ListOfTools.jsx +++ b/frontend/src/components/custom-tools/list-of-tools/ListOfTools.jsx @@ -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")); @@ -384,6 +391,7 @@ function ListOfTools() { loading={isShareLoading} allUsers={allUserList} onApply={onShare} + isSharableToOrg={true} /> );