ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: ручное назначение Voice Tasker
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from plane.db.models import Project, WorkspaceAICredential, WorkspaceAISettings
|
||||
from plane.db.models import Project, WorkspaceAICredential, WorkspaceAISettings, WorkspaceMember
|
||||
from plane.license.utils.encryption import encrypt_data
|
||||
|
||||
from .base import BaseSerializer
|
||||
@@ -12,6 +12,8 @@ from .base import BaseSerializer
|
||||
|
||||
class WorkspaceAISettingsSerializer(BaseSerializer):
|
||||
default_project_id = serializers.UUIDField(required=False, allow_null=True)
|
||||
enabled_project_ids = serializers.ListField(child=serializers.UUIDField(), required=False, write_only=True)
|
||||
enabled_member_ids = serializers.ListField(child=serializers.UUIDField(), required=False, write_only=True)
|
||||
openai_api_key = serializers.CharField(required=False, allow_blank=True, write_only=True, trim_whitespace=False)
|
||||
credential = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
@@ -26,6 +28,8 @@ class WorkspaceAISettingsSerializer(BaseSerializer):
|
||||
"structuring_model",
|
||||
"default_project_id",
|
||||
"access_mode",
|
||||
"enabled_project_ids",
|
||||
"enabled_member_ids",
|
||||
"max_audio_duration_seconds",
|
||||
"per_user_hourly_limit",
|
||||
"workspace_hourly_limit",
|
||||
@@ -45,6 +49,12 @@ class WorkspaceAISettingsSerializer(BaseSerializer):
|
||||
"updated_at": credential.updated_at if credential else None,
|
||||
}
|
||||
|
||||
def to_representation(self, instance):
|
||||
data = super().to_representation(instance)
|
||||
data["enabled_project_ids"] = [str(project_id) for project_id in instance.enabled_projects.values_list("id", flat=True)]
|
||||
data["enabled_member_ids"] = [str(member_id) for member_id in instance.enabled_members.values_list("id", flat=True)]
|
||||
return data
|
||||
|
||||
def validate_default_project_id(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
@@ -54,6 +64,32 @@ class WorkspaceAISettingsSerializer(BaseSerializer):
|
||||
raise serializers.ValidationError("Default project must belong to this workspace.")
|
||||
return value
|
||||
|
||||
def validate_enabled_project_ids(self, value):
|
||||
workspace = self.context["workspace"]
|
||||
project_ids = list(dict.fromkeys(value))
|
||||
existing_ids = set(
|
||||
Project.objects.filter(workspace=workspace, id__in=project_ids, archived_at__isnull=True).values_list(
|
||||
"id", flat=True
|
||||
)
|
||||
)
|
||||
if len(existing_ids) != len(project_ids):
|
||||
raise serializers.ValidationError("All selected projects must belong to this workspace.")
|
||||
return project_ids
|
||||
|
||||
def validate_enabled_member_ids(self, value):
|
||||
workspace = self.context["workspace"]
|
||||
member_ids = list(dict.fromkeys(value))
|
||||
existing_ids = set(
|
||||
WorkspaceMember.objects.filter(
|
||||
workspace=workspace,
|
||||
member_id__in=member_ids,
|
||||
is_active=True,
|
||||
).values_list("member_id", flat=True)
|
||||
)
|
||||
if len(existing_ids) != len(member_ids):
|
||||
raise serializers.ValidationError("All selected members must be active workspace members.")
|
||||
return member_ids
|
||||
|
||||
def validate_max_audio_duration_seconds(self, value):
|
||||
if value < 10 or value > 600:
|
||||
raise serializers.ValidationError("Max audio duration must be between 10 and 600 seconds.")
|
||||
@@ -72,6 +108,8 @@ class WorkspaceAISettingsSerializer(BaseSerializer):
|
||||
def update(self, instance, validated_data):
|
||||
api_key = validated_data.pop("openai_api_key", None)
|
||||
default_project_id = validated_data.pop("default_project_id", serializers.empty)
|
||||
enabled_project_ids = validated_data.pop("enabled_project_ids", serializers.empty)
|
||||
enabled_member_ids = validated_data.pop("enabled_member_ids", serializers.empty)
|
||||
|
||||
if default_project_id is not serializers.empty:
|
||||
instance.default_project_id = default_project_id
|
||||
@@ -81,6 +119,12 @@ class WorkspaceAISettingsSerializer(BaseSerializer):
|
||||
|
||||
instance.save()
|
||||
|
||||
if enabled_project_ids is not serializers.empty:
|
||||
instance.enabled_projects.set(enabled_project_ids)
|
||||
|
||||
if enabled_member_ids is not serializers.empty:
|
||||
instance.enabled_members.set(enabled_member_ids)
|
||||
|
||||
if api_key:
|
||||
cleaned_api_key = api_key.strip()
|
||||
credential, _ = WorkspaceAICredential.objects.get_or_create(
|
||||
|
||||
@@ -227,7 +227,30 @@ def normalize_audio_content_type(content_type):
|
||||
return content_type.split(";")[0].strip().lower()
|
||||
|
||||
|
||||
def get_voice_task_preflight(workspace, user):
|
||||
def get_request_project_id(request):
|
||||
project_id = request.query_params.get("project_id")
|
||||
if project_id:
|
||||
return project_id
|
||||
|
||||
draft = request.data.get("draft")
|
||||
if isinstance(draft, dict) and draft.get("project_id"):
|
||||
return draft.get("project_id")
|
||||
|
||||
client_context = request.data.get("client_context")
|
||||
if not client_context:
|
||||
return None
|
||||
|
||||
try:
|
||||
parsed_context = json.loads(client_context) if isinstance(client_context, str) else client_context
|
||||
except (TypeError, json.JSONDecodeError):
|
||||
return None
|
||||
|
||||
if isinstance(parsed_context, dict):
|
||||
return parsed_context.get("current_project_id")
|
||||
return None
|
||||
|
||||
|
||||
def get_voice_task_preflight(workspace, user, project_id=None):
|
||||
ai_settings = WorkspaceAISettings.objects.filter(workspace=workspace).first()
|
||||
workspace_member = WorkspaceMember.objects.filter(workspace=workspace, member=user, is_active=True).first()
|
||||
|
||||
@@ -237,6 +260,8 @@ def get_voice_task_preflight(workspace, user):
|
||||
"max_audio_duration_seconds": 120,
|
||||
"accepted_mime_types": VOICE_TASK_ACCEPTED_AUDIO_TYPES,
|
||||
"access_mode": "all_workspace_members",
|
||||
"enabled_project_ids": [],
|
||||
"enabled_member_ids": [],
|
||||
}
|
||||
|
||||
if not ai_settings:
|
||||
@@ -244,6 +269,8 @@ def get_voice_task_preflight(workspace, user):
|
||||
|
||||
response["max_audio_duration_seconds"] = ai_settings.max_audio_duration_seconds
|
||||
response["access_mode"] = ai_settings.access_mode
|
||||
response["enabled_project_ids"] = [str(project_id) for project_id in ai_settings.enabled_projects.values_list("id", flat=True)]
|
||||
response["enabled_member_ids"] = [str(member_id) for member_id in ai_settings.enabled_members.values_list("id", flat=True)]
|
||||
|
||||
if not ai_settings.voice_tasker_enabled:
|
||||
response["reason"] = "disabled"
|
||||
@@ -263,6 +290,19 @@ def get_voice_task_preflight(workspace, user):
|
||||
response["reason"] = "role_denied"
|
||||
return response
|
||||
|
||||
if ai_settings.access_mode == WorkspaceAISettings.AccessMode.SELECTED_MEMBERS:
|
||||
if not ai_settings.enabled_members.filter(id=user.id).exists():
|
||||
response["reason"] = "scope_denied"
|
||||
return response
|
||||
|
||||
if ai_settings.access_mode == WorkspaceAISettings.AccessMode.SELECTED_PROJECTS:
|
||||
if not project_id:
|
||||
response["reason"] = "scope_denied"
|
||||
return response
|
||||
if not ai_settings.enabled_projects.filter(id=project_id, workspace=workspace, archived_at__isnull=True).exists():
|
||||
response["reason"] = "scope_denied"
|
||||
return response
|
||||
|
||||
response["available"] = True
|
||||
response["reason"] = None
|
||||
return response
|
||||
@@ -2332,7 +2372,10 @@ class VoiceTaskPreflightEndpoint(BaseAPIView):
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def get(self, request, slug):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
return Response(get_voice_task_preflight(workspace, request.user), status=status.HTTP_200_OK)
|
||||
return Response(
|
||||
get_voice_task_preflight(workspace, request.user, project_id=get_request_project_id(request)),
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
class VoiceTaskParseEndpoint(BaseAPIView):
|
||||
@@ -2341,10 +2384,14 @@ class VoiceTaskParseEndpoint(BaseAPIView):
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def post(self, request, slug):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
preflight = get_voice_task_preflight(workspace, request.user)
|
||||
preflight = get_voice_task_preflight(workspace, request.user, project_id=get_request_project_id(request))
|
||||
|
||||
if not preflight["available"]:
|
||||
response_status = status.HTTP_403_FORBIDDEN if preflight["reason"] == "role_denied" else status.HTTP_400_BAD_REQUEST
|
||||
response_status = (
|
||||
status.HTTP_403_FORBIDDEN
|
||||
if preflight["reason"] in {"role_denied", "scope_denied"}
|
||||
else status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
"ok": False,
|
||||
@@ -2512,9 +2559,13 @@ class VoiceTaskCommitEndpoint(BaseAPIView):
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def post(self, request, slug):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
preflight = get_voice_task_preflight(workspace, request.user)
|
||||
preflight = get_voice_task_preflight(workspace, request.user, project_id=get_request_project_id(request))
|
||||
if not preflight["available"]:
|
||||
response_status = status.HTTP_403_FORBIDDEN if preflight["reason"] == "role_denied" else status.HTTP_400_BAD_REQUEST
|
||||
response_status = (
|
||||
status.HTTP_403_FORBIDDEN
|
||||
if preflight["reason"] in {"role_denied", "scope_denied"}
|
||||
else status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
"ok": False,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# Generated by Codex on 2026-04-28
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("db", "0128_stored_blob_dedup"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="workspaceaisettings",
|
||||
name="access_mode",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("all_workspace_members", "All workspace members"),
|
||||
("admins_only", "Admins only"),
|
||||
("selected_projects", "Selected projects"),
|
||||
("selected_members", "Selected members"),
|
||||
],
|
||||
default="all_workspace_members",
|
||||
max_length=40,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="workspaceaisettings",
|
||||
name="enabled_members",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="workspace_ai_feature_settings",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="workspaceaisettings",
|
||||
name="enabled_projects",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="workspace_ai_feature_settings",
|
||||
to="db.project",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -15,6 +15,8 @@ class WorkspaceAISettings(BaseModel):
|
||||
class AccessMode(models.TextChoices):
|
||||
ALL_WORKSPACE_MEMBERS = "all_workspace_members", "All workspace members"
|
||||
ADMINS_ONLY = "admins_only", "Admins only"
|
||||
SELECTED_PROJECTS = "selected_projects", "Selected projects"
|
||||
SELECTED_MEMBERS = "selected_members", "Selected members"
|
||||
|
||||
workspace = models.OneToOneField(
|
||||
"db.Workspace",
|
||||
@@ -37,6 +39,16 @@ class WorkspaceAISettings(BaseModel):
|
||||
choices=AccessMode.choices,
|
||||
default=AccessMode.ALL_WORKSPACE_MEMBERS,
|
||||
)
|
||||
enabled_projects = models.ManyToManyField(
|
||||
"db.Project",
|
||||
blank=True,
|
||||
related_name="workspace_ai_feature_settings",
|
||||
)
|
||||
enabled_members = models.ManyToManyField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
blank=True,
|
||||
related_name="workspace_ai_feature_settings",
|
||||
)
|
||||
max_audio_duration_seconds = models.PositiveIntegerField(default=120)
|
||||
per_user_hourly_limit = models.PositiveIntegerField(default=30)
|
||||
workspace_hourly_limit = models.PositiveIntegerField(default=300)
|
||||
|
||||
Reference in New Issue
Block a user