django-agent-studio 0.3.1__py3-none-any.whl → 0.3.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_agent_studio/api/serializers.py +139 -0
- django_agent_studio/api/urls.py +35 -0
- django_agent_studio/api/views.py +470 -8
- django_agent_studio/static/agent-frontend/chat-widget.css +6 -1
- django_agent_studio/templates/django_agent_studio/agent_list.html +12 -5
- django_agent_studio/templates/django_agent_studio/base.html +12 -0
- django_agent_studio/templates/django_agent_studio/collaborators.html +418 -0
- django_agent_studio/templates/django_agent_studio/home.html +21 -12
- django_agent_studio/templates/django_agent_studio/system_create.html +148 -0
- django_agent_studio/templates/django_agent_studio/system_list.html +15 -3
- django_agent_studio/templates/django_agent_studio/system_test.html +19 -6
- django_agent_studio/templates/django_agent_studio/test.html +18 -5
- django_agent_studio/urls.py +4 -0
- django_agent_studio/views.py +317 -15
- {django_agent_studio-0.3.1.dist-info → django_agent_studio-0.3.3.dist-info}/METADATA +3 -1
- {django_agent_studio-0.3.1.dist-info → django_agent_studio-0.3.3.dist-info}/RECORD +19 -17
- {django_agent_studio-0.3.1.dist-info → django_agent_studio-0.3.3.dist-info}/WHEEL +0 -0
- {django_agent_studio-0.3.1.dist-info → django_agent_studio-0.3.3.dist-info}/licenses/LICENSE +0 -0
- {django_agent_studio-0.3.1.dist-info → django_agent_studio-0.3.3.dist-info}/top_level.txt +0 -0
django_agent_studio/api/views.py
CHANGED
|
@@ -24,6 +24,10 @@ from django_agent_runtime.models import (
|
|
|
24
24
|
AgentSystemMember,
|
|
25
25
|
AgentSystemVersion,
|
|
26
26
|
AgentSystemSnapshot,
|
|
27
|
+
# Collaborator models
|
|
28
|
+
CollaboratorRole,
|
|
29
|
+
AgentCollaborator,
|
|
30
|
+
SystemCollaborator,
|
|
27
31
|
)
|
|
28
32
|
from django_agent_studio.api.serializers import (
|
|
29
33
|
AgentDefinitionListSerializer,
|
|
@@ -36,6 +40,11 @@ from django_agent_studio.api.serializers import (
|
|
|
36
40
|
DynamicToolExecutionSerializer,
|
|
37
41
|
ProjectScanRequestSerializer,
|
|
38
42
|
GenerateToolsRequestSerializer,
|
|
43
|
+
# Collaborator serializers
|
|
44
|
+
AgentCollaboratorSerializer,
|
|
45
|
+
SystemCollaboratorSerializer,
|
|
46
|
+
AddCollaboratorSerializer,
|
|
47
|
+
UpdateCollaboratorRoleSerializer,
|
|
39
48
|
)
|
|
40
49
|
|
|
41
50
|
|
|
@@ -43,16 +52,49 @@ from django_agent_studio.api.serializers import (
|
|
|
43
52
|
# Helper functions for consistent access control
|
|
44
53
|
# =============================================================================
|
|
45
54
|
|
|
55
|
+
from django.db.models import Q
|
|
56
|
+
|
|
57
|
+
|
|
46
58
|
def get_agent_for_user(user, agent_id):
|
|
47
59
|
"""
|
|
48
60
|
Get an agent if the user has access.
|
|
49
61
|
|
|
50
62
|
- Superusers can access any agent
|
|
51
|
-
-
|
|
63
|
+
- Owners can access their own agents
|
|
64
|
+
- Collaborators can access agents they've been granted access to
|
|
65
|
+
- Users with system access can access agents in that system
|
|
52
66
|
"""
|
|
53
67
|
if user.is_superuser:
|
|
54
68
|
return get_object_or_404(AgentDefinition, id=agent_id)
|
|
55
|
-
|
|
69
|
+
|
|
70
|
+
# Try to get the agent
|
|
71
|
+
agent = get_object_or_404(AgentDefinition, id=agent_id)
|
|
72
|
+
|
|
73
|
+
# Check if owner
|
|
74
|
+
if agent.owner == user:
|
|
75
|
+
return agent
|
|
76
|
+
|
|
77
|
+
# Check if direct collaborator on agent
|
|
78
|
+
if AgentCollaborator.objects.filter(agent=agent, user=user).exists():
|
|
79
|
+
return agent
|
|
80
|
+
|
|
81
|
+
# Check if user has access via a system that contains this agent
|
|
82
|
+
# Get all systems this agent belongs to
|
|
83
|
+
agent_system_ids = AgentSystemMember.objects.filter(
|
|
84
|
+
agent=agent
|
|
85
|
+
).values_list('system_id', flat=True)
|
|
86
|
+
|
|
87
|
+
if agent_system_ids:
|
|
88
|
+
# Check if user owns or is a collaborator on any of these systems
|
|
89
|
+
has_system_access = AgentSystem.objects.filter(
|
|
90
|
+
Q(id__in=agent_system_ids) &
|
|
91
|
+
(Q(owner=user) | Q(collaborators__user=user))
|
|
92
|
+
).exists()
|
|
93
|
+
if has_system_access:
|
|
94
|
+
return agent
|
|
95
|
+
|
|
96
|
+
# No access
|
|
97
|
+
raise Http404("Agent not found")
|
|
56
98
|
|
|
57
99
|
|
|
58
100
|
def get_agent_queryset_for_user(user):
|
|
@@ -60,11 +102,29 @@ def get_agent_queryset_for_user(user):
|
|
|
60
102
|
Get queryset of agents accessible to the user.
|
|
61
103
|
|
|
62
104
|
- Superusers can see all agents
|
|
63
|
-
- Regular users see
|
|
105
|
+
- Regular users see:
|
|
106
|
+
- Owned agents
|
|
107
|
+
- Agents where they are direct collaborators
|
|
108
|
+
- Agents in systems they own or are collaborators on
|
|
64
109
|
"""
|
|
65
110
|
if user.is_superuser:
|
|
66
111
|
return AgentDefinition.objects.all()
|
|
67
|
-
|
|
112
|
+
|
|
113
|
+
# Get system IDs the user has access to (owner or collaborator)
|
|
114
|
+
accessible_system_ids = AgentSystem.objects.filter(
|
|
115
|
+
Q(owner=user) | Q(collaborators__user=user)
|
|
116
|
+
).values_list('id', flat=True)
|
|
117
|
+
|
|
118
|
+
# Get agent IDs that are members of accessible systems
|
|
119
|
+
agents_via_systems = AgentSystemMember.objects.filter(
|
|
120
|
+
system_id__in=accessible_system_ids
|
|
121
|
+
).values_list('agent_id', flat=True)
|
|
122
|
+
|
|
123
|
+
return AgentDefinition.objects.filter(
|
|
124
|
+
Q(owner=user) |
|
|
125
|
+
Q(collaborators__user=user) |
|
|
126
|
+
Q(id__in=agents_via_systems)
|
|
127
|
+
).distinct()
|
|
68
128
|
|
|
69
129
|
|
|
70
130
|
def get_system_for_user(user, system_id):
|
|
@@ -72,11 +132,27 @@ def get_system_for_user(user, system_id):
|
|
|
72
132
|
Get a system if the user has access.
|
|
73
133
|
|
|
74
134
|
- Superusers can access any system
|
|
75
|
-
-
|
|
135
|
+
- Owners can access their own systems
|
|
136
|
+
- Collaborators can access systems they've been granted access to
|
|
76
137
|
"""
|
|
138
|
+
from django_agent_runtime.models import SystemCollaborator
|
|
139
|
+
|
|
77
140
|
if user.is_superuser:
|
|
78
141
|
return get_object_or_404(AgentSystem, id=system_id)
|
|
79
|
-
|
|
142
|
+
|
|
143
|
+
# Try to get the system
|
|
144
|
+
system = get_object_or_404(AgentSystem, id=system_id)
|
|
145
|
+
|
|
146
|
+
# Check if owner
|
|
147
|
+
if system.owner == user:
|
|
148
|
+
return system
|
|
149
|
+
|
|
150
|
+
# Check if collaborator
|
|
151
|
+
if SystemCollaborator.objects.filter(system=system, user=user).exists():
|
|
152
|
+
return system
|
|
153
|
+
|
|
154
|
+
# No access
|
|
155
|
+
raise Http404("System not found")
|
|
80
156
|
|
|
81
157
|
|
|
82
158
|
def get_system_queryset_for_user(user):
|
|
@@ -84,11 +160,13 @@ def get_system_queryset_for_user(user):
|
|
|
84
160
|
Get queryset of systems accessible to the user.
|
|
85
161
|
|
|
86
162
|
- Superusers can see all systems
|
|
87
|
-
- Regular users see
|
|
163
|
+
- Regular users see owned systems and systems where they are collaborators
|
|
88
164
|
"""
|
|
89
165
|
if user.is_superuser:
|
|
90
166
|
return AgentSystem.objects.all()
|
|
91
|
-
return AgentSystem.objects.filter(
|
|
167
|
+
return AgentSystem.objects.filter(
|
|
168
|
+
Q(owner=user) | Q(collaborators__user=user)
|
|
169
|
+
).distinct()
|
|
92
170
|
|
|
93
171
|
|
|
94
172
|
class AgentDefinitionListCreateView(generics.ListCreateAPIView):
|
|
@@ -2068,3 +2146,387 @@ class AgentSpecDocumentView(APIView):
|
|
|
2068
2146
|
'created': created,
|
|
2069
2147
|
'message': f"Spec {'created' if created else 'updated'} for {agent.name}",
|
|
2070
2148
|
})
|
|
2149
|
+
|
|
2150
|
+
|
|
2151
|
+
# =============================================================================
|
|
2152
|
+
# Collaborator Management Views
|
|
2153
|
+
# =============================================================================
|
|
2154
|
+
|
|
2155
|
+
from django_agent_runtime.models import (
|
|
2156
|
+
AgentCollaborator,
|
|
2157
|
+
SystemCollaborator,
|
|
2158
|
+
CollaboratorRole,
|
|
2159
|
+
)
|
|
2160
|
+
from django_agent_studio.api.serializers import (
|
|
2161
|
+
AgentCollaboratorSerializer,
|
|
2162
|
+
SystemCollaboratorSerializer,
|
|
2163
|
+
AddCollaboratorSerializer,
|
|
2164
|
+
UpdateCollaboratorRoleSerializer,
|
|
2165
|
+
)
|
|
2166
|
+
|
|
2167
|
+
|
|
2168
|
+
class AgentCollaboratorListCreateView(generics.ListCreateAPIView):
|
|
2169
|
+
"""
|
|
2170
|
+
List and add collaborators to an agent.
|
|
2171
|
+
|
|
2172
|
+
GET /api/agents/{id}/collaborators/
|
|
2173
|
+
POST /api/agents/{id}/collaborators/
|
|
2174
|
+
|
|
2175
|
+
Only owners and admins can manage collaborators.
|
|
2176
|
+
"""
|
|
2177
|
+
|
|
2178
|
+
permission_classes = [IsAuthenticated]
|
|
2179
|
+
|
|
2180
|
+
def get_serializer_class(self):
|
|
2181
|
+
if self.request.method == 'POST':
|
|
2182
|
+
return AddCollaboratorSerializer
|
|
2183
|
+
return AgentCollaboratorSerializer
|
|
2184
|
+
|
|
2185
|
+
def get_queryset(self):
|
|
2186
|
+
agent = get_agent_for_user(self.request.user, self.kwargs['agent_id'])
|
|
2187
|
+
return agent.collaborators.select_related('user', 'added_by').all()
|
|
2188
|
+
|
|
2189
|
+
def create(self, request, *args, **kwargs):
|
|
2190
|
+
agent = get_agent_for_user(request.user, self.kwargs['agent_id'])
|
|
2191
|
+
|
|
2192
|
+
# Check if user can manage collaborators (owner or admin)
|
|
2193
|
+
if not self._can_admin(request.user, agent):
|
|
2194
|
+
return Response(
|
|
2195
|
+
{'error': 'Only owners and admins can manage collaborators'},
|
|
2196
|
+
status=status.HTTP_403_FORBIDDEN,
|
|
2197
|
+
)
|
|
2198
|
+
|
|
2199
|
+
serializer = self.get_serializer(data=request.data)
|
|
2200
|
+
serializer.is_valid(raise_exception=True)
|
|
2201
|
+
|
|
2202
|
+
# Find user by email or username
|
|
2203
|
+
from django.contrib.auth import get_user_model
|
|
2204
|
+
User = get_user_model()
|
|
2205
|
+
identifier = serializer.validated_data['email']
|
|
2206
|
+
|
|
2207
|
+
# Try to find user by email first, then username
|
|
2208
|
+
user = None
|
|
2209
|
+
user_fields = [f.name for f in User._meta.get_fields()]
|
|
2210
|
+
|
|
2211
|
+
if 'email' in user_fields:
|
|
2212
|
+
user = User.objects.filter(email=identifier).first()
|
|
2213
|
+
if not user and 'username' in user_fields:
|
|
2214
|
+
user = User.objects.filter(username=identifier).first()
|
|
2215
|
+
|
|
2216
|
+
if not user:
|
|
2217
|
+
return Response(
|
|
2218
|
+
{'error': f"User '{identifier}' not found"},
|
|
2219
|
+
status=status.HTTP_404_NOT_FOUND,
|
|
2220
|
+
)
|
|
2221
|
+
|
|
2222
|
+
# Can't add owner as collaborator
|
|
2223
|
+
if user == agent.owner:
|
|
2224
|
+
return Response(
|
|
2225
|
+
{'error': 'Cannot add the owner as a collaborator'},
|
|
2226
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
2227
|
+
)
|
|
2228
|
+
|
|
2229
|
+
# Check if already a collaborator
|
|
2230
|
+
if AgentCollaborator.objects.filter(agent=agent, user=user).exists():
|
|
2231
|
+
return Response(
|
|
2232
|
+
{'error': 'User is already a collaborator on this agent'},
|
|
2233
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
2234
|
+
)
|
|
2235
|
+
|
|
2236
|
+
# Create collaborator
|
|
2237
|
+
collaborator = AgentCollaborator.objects.create(
|
|
2238
|
+
agent=agent,
|
|
2239
|
+
user=user,
|
|
2240
|
+
role=serializer.validated_data.get('role', CollaboratorRole.VIEWER),
|
|
2241
|
+
added_by=request.user,
|
|
2242
|
+
)
|
|
2243
|
+
|
|
2244
|
+
return Response(
|
|
2245
|
+
AgentCollaboratorSerializer(collaborator).data,
|
|
2246
|
+
status=status.HTTP_201_CREATED,
|
|
2247
|
+
)
|
|
2248
|
+
|
|
2249
|
+
def _can_admin(self, user, agent):
|
|
2250
|
+
"""Check if user can manage collaborators."""
|
|
2251
|
+
if user.is_superuser or agent.owner == user:
|
|
2252
|
+
return True
|
|
2253
|
+
try:
|
|
2254
|
+
collab = AgentCollaborator.objects.get(agent=agent, user=user)
|
|
2255
|
+
return collab.can_admin
|
|
2256
|
+
except AgentCollaborator.DoesNotExist:
|
|
2257
|
+
return False
|
|
2258
|
+
|
|
2259
|
+
|
|
2260
|
+
class AgentCollaboratorDetailView(generics.RetrieveUpdateDestroyAPIView):
|
|
2261
|
+
"""
|
|
2262
|
+
Retrieve, update, or delete an agent collaborator.
|
|
2263
|
+
|
|
2264
|
+
GET/PUT/PATCH/DELETE /api/agents/{id}/collaborators/{collaborator_id}/
|
|
2265
|
+
|
|
2266
|
+
Only owners and admins can manage collaborators.
|
|
2267
|
+
"""
|
|
2268
|
+
|
|
2269
|
+
permission_classes = [IsAuthenticated]
|
|
2270
|
+
serializer_class = AgentCollaboratorSerializer
|
|
2271
|
+
|
|
2272
|
+
def get_queryset(self):
|
|
2273
|
+
agent = get_agent_for_user(self.request.user, self.kwargs['agent_id'])
|
|
2274
|
+
return agent.collaborators.select_related('user', 'added_by').all()
|
|
2275
|
+
|
|
2276
|
+
def update(self, request, *args, **kwargs):
|
|
2277
|
+
agent = get_agent_for_user(request.user, self.kwargs['agent_id'])
|
|
2278
|
+
|
|
2279
|
+
if not self._can_admin(request.user, agent):
|
|
2280
|
+
return Response(
|
|
2281
|
+
{'error': 'Only owners and admins can manage collaborators'},
|
|
2282
|
+
status=status.HTTP_403_FORBIDDEN,
|
|
2283
|
+
)
|
|
2284
|
+
|
|
2285
|
+
# Use the role update serializer for validation
|
|
2286
|
+
role_serializer = UpdateCollaboratorRoleSerializer(data=request.data)
|
|
2287
|
+
role_serializer.is_valid(raise_exception=True)
|
|
2288
|
+
|
|
2289
|
+
collaborator = self.get_object()
|
|
2290
|
+
collaborator.role = role_serializer.validated_data['role']
|
|
2291
|
+
collaborator.save()
|
|
2292
|
+
|
|
2293
|
+
return Response(AgentCollaboratorSerializer(collaborator).data)
|
|
2294
|
+
|
|
2295
|
+
def destroy(self, request, *args, **kwargs):
|
|
2296
|
+
agent = get_agent_for_user(request.user, self.kwargs['agent_id'])
|
|
2297
|
+
|
|
2298
|
+
if not self._can_admin(request.user, agent):
|
|
2299
|
+
return Response(
|
|
2300
|
+
{'error': 'Only owners and admins can manage collaborators'},
|
|
2301
|
+
status=status.HTTP_403_FORBIDDEN,
|
|
2302
|
+
)
|
|
2303
|
+
|
|
2304
|
+
return super().destroy(request, *args, **kwargs)
|
|
2305
|
+
|
|
2306
|
+
def _can_admin(self, user, agent):
|
|
2307
|
+
"""Check if user can manage collaborators."""
|
|
2308
|
+
if user.is_superuser or agent.owner == user:
|
|
2309
|
+
return True
|
|
2310
|
+
try:
|
|
2311
|
+
collab = AgentCollaborator.objects.get(agent=agent, user=user)
|
|
2312
|
+
return collab.can_admin
|
|
2313
|
+
except AgentCollaborator.DoesNotExist:
|
|
2314
|
+
return False
|
|
2315
|
+
|
|
2316
|
+
|
|
2317
|
+
class SystemCollaboratorListCreateView(generics.ListCreateAPIView):
|
|
2318
|
+
"""
|
|
2319
|
+
List and add collaborators to a system.
|
|
2320
|
+
|
|
2321
|
+
GET /api/systems/{id}/collaborators/
|
|
2322
|
+
POST /api/systems/{id}/collaborators/
|
|
2323
|
+
|
|
2324
|
+
Only owners and admins can manage collaborators.
|
|
2325
|
+
"""
|
|
2326
|
+
|
|
2327
|
+
permission_classes = [IsAuthenticated]
|
|
2328
|
+
|
|
2329
|
+
def get_serializer_class(self):
|
|
2330
|
+
if self.request.method == 'POST':
|
|
2331
|
+
return AddCollaboratorSerializer
|
|
2332
|
+
return SystemCollaboratorSerializer
|
|
2333
|
+
|
|
2334
|
+
def get_queryset(self):
|
|
2335
|
+
system = get_system_for_user(self.request.user, self.kwargs['system_id'])
|
|
2336
|
+
return system.collaborators.select_related('user', 'added_by').all()
|
|
2337
|
+
|
|
2338
|
+
def create(self, request, *args, **kwargs):
|
|
2339
|
+
system = get_system_for_user(request.user, self.kwargs['system_id'])
|
|
2340
|
+
|
|
2341
|
+
# Check if user can manage collaborators (owner or admin)
|
|
2342
|
+
if not self._can_admin(request.user, system):
|
|
2343
|
+
return Response(
|
|
2344
|
+
{'error': 'Only owners and admins can manage collaborators'},
|
|
2345
|
+
status=status.HTTP_403_FORBIDDEN,
|
|
2346
|
+
)
|
|
2347
|
+
|
|
2348
|
+
serializer = self.get_serializer(data=request.data)
|
|
2349
|
+
serializer.is_valid(raise_exception=True)
|
|
2350
|
+
|
|
2351
|
+
# Find user by email or username
|
|
2352
|
+
from django.contrib.auth import get_user_model
|
|
2353
|
+
User = get_user_model()
|
|
2354
|
+
identifier = serializer.validated_data['email']
|
|
2355
|
+
|
|
2356
|
+
# Try to find user by email first, then username
|
|
2357
|
+
user = None
|
|
2358
|
+
user_fields = [f.name for f in User._meta.get_fields()]
|
|
2359
|
+
|
|
2360
|
+
if 'email' in user_fields:
|
|
2361
|
+
user = User.objects.filter(email=identifier).first()
|
|
2362
|
+
if not user and 'username' in user_fields:
|
|
2363
|
+
user = User.objects.filter(username=identifier).first()
|
|
2364
|
+
|
|
2365
|
+
if not user:
|
|
2366
|
+
return Response(
|
|
2367
|
+
{'error': f"User '{identifier}' not found"},
|
|
2368
|
+
status=status.HTTP_404_NOT_FOUND,
|
|
2369
|
+
)
|
|
2370
|
+
|
|
2371
|
+
# Can't add owner as collaborator
|
|
2372
|
+
if user == system.owner:
|
|
2373
|
+
return Response(
|
|
2374
|
+
{'error': 'Cannot add the owner as a collaborator'},
|
|
2375
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
2376
|
+
)
|
|
2377
|
+
|
|
2378
|
+
# Check if already a collaborator
|
|
2379
|
+
if SystemCollaborator.objects.filter(system=system, user=user).exists():
|
|
2380
|
+
return Response(
|
|
2381
|
+
{'error': 'User is already a collaborator on this system'},
|
|
2382
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
2383
|
+
)
|
|
2384
|
+
|
|
2385
|
+
# Create collaborator
|
|
2386
|
+
collaborator = SystemCollaborator.objects.create(
|
|
2387
|
+
system=system,
|
|
2388
|
+
user=user,
|
|
2389
|
+
role=serializer.validated_data.get('role', CollaboratorRole.VIEWER),
|
|
2390
|
+
added_by=request.user,
|
|
2391
|
+
)
|
|
2392
|
+
|
|
2393
|
+
return Response(
|
|
2394
|
+
SystemCollaboratorSerializer(collaborator).data,
|
|
2395
|
+
status=status.HTTP_201_CREATED,
|
|
2396
|
+
)
|
|
2397
|
+
|
|
2398
|
+
def _can_admin(self, user, system):
|
|
2399
|
+
"""Check if user can manage collaborators."""
|
|
2400
|
+
if user.is_superuser or system.owner == user:
|
|
2401
|
+
return True
|
|
2402
|
+
try:
|
|
2403
|
+
collab = SystemCollaborator.objects.get(system=system, user=user)
|
|
2404
|
+
return collab.can_admin
|
|
2405
|
+
except SystemCollaborator.DoesNotExist:
|
|
2406
|
+
return False
|
|
2407
|
+
|
|
2408
|
+
|
|
2409
|
+
class SystemCollaboratorDetailView(generics.RetrieveUpdateDestroyAPIView):
|
|
2410
|
+
"""
|
|
2411
|
+
Retrieve, update, or delete a system collaborator.
|
|
2412
|
+
|
|
2413
|
+
GET/PUT/PATCH/DELETE /api/systems/{id}/collaborators/{collaborator_id}/
|
|
2414
|
+
|
|
2415
|
+
Only owners and admins can manage collaborators.
|
|
2416
|
+
"""
|
|
2417
|
+
|
|
2418
|
+
permission_classes = [IsAuthenticated]
|
|
2419
|
+
serializer_class = SystemCollaboratorSerializer
|
|
2420
|
+
|
|
2421
|
+
def get_queryset(self):
|
|
2422
|
+
system = get_system_for_user(self.request.user, self.kwargs['system_id'])
|
|
2423
|
+
return system.collaborators.select_related('user', 'added_by').all()
|
|
2424
|
+
|
|
2425
|
+
def update(self, request, *args, **kwargs):
|
|
2426
|
+
system = get_system_for_user(request.user, self.kwargs['system_id'])
|
|
2427
|
+
|
|
2428
|
+
if not self._can_admin(request.user, system):
|
|
2429
|
+
return Response(
|
|
2430
|
+
{'error': 'Only owners and admins can manage collaborators'},
|
|
2431
|
+
status=status.HTTP_403_FORBIDDEN,
|
|
2432
|
+
)
|
|
2433
|
+
|
|
2434
|
+
# Use the role update serializer for validation
|
|
2435
|
+
role_serializer = UpdateCollaboratorRoleSerializer(data=request.data)
|
|
2436
|
+
role_serializer.is_valid(raise_exception=True)
|
|
2437
|
+
|
|
2438
|
+
collaborator = self.get_object()
|
|
2439
|
+
collaborator.role = role_serializer.validated_data['role']
|
|
2440
|
+
collaborator.save()
|
|
2441
|
+
|
|
2442
|
+
return Response(SystemCollaboratorSerializer(collaborator).data)
|
|
2443
|
+
|
|
2444
|
+
def destroy(self, request, *args, **kwargs):
|
|
2445
|
+
system = get_system_for_user(request.user, self.kwargs['system_id'])
|
|
2446
|
+
|
|
2447
|
+
if not self._can_admin(request.user, system):
|
|
2448
|
+
return Response(
|
|
2449
|
+
{'error': 'Only owners and admins can manage collaborators'},
|
|
2450
|
+
status=status.HTTP_403_FORBIDDEN,
|
|
2451
|
+
)
|
|
2452
|
+
|
|
2453
|
+
return super().destroy(request, *args, **kwargs)
|
|
2454
|
+
|
|
2455
|
+
def _can_admin(self, user, system):
|
|
2456
|
+
"""Check if user can manage collaborators."""
|
|
2457
|
+
if user.is_superuser or system.owner == user:
|
|
2458
|
+
return True
|
|
2459
|
+
try:
|
|
2460
|
+
collab = SystemCollaborator.objects.get(system=system, user=user)
|
|
2461
|
+
return collab.can_admin
|
|
2462
|
+
except SystemCollaborator.DoesNotExist:
|
|
2463
|
+
return False
|
|
2464
|
+
|
|
2465
|
+
|
|
2466
|
+
class UserSearchView(APIView):
|
|
2467
|
+
"""
|
|
2468
|
+
Search for users by email, username, or name for collaborator autocomplete.
|
|
2469
|
+
|
|
2470
|
+
GET /api/users/search/?q=<query>
|
|
2471
|
+
|
|
2472
|
+
Returns up to 10 matching users.
|
|
2473
|
+
Handles both email-based and username-based User models.
|
|
2474
|
+
"""
|
|
2475
|
+
|
|
2476
|
+
permission_classes = [IsAuthenticated]
|
|
2477
|
+
|
|
2478
|
+
def get(self, request):
|
|
2479
|
+
from django.contrib.auth import get_user_model
|
|
2480
|
+
from django.db.models import Q
|
|
2481
|
+
|
|
2482
|
+
User = get_user_model()
|
|
2483
|
+
query = request.query_params.get('q', '').strip()
|
|
2484
|
+
|
|
2485
|
+
if len(query) < 2:
|
|
2486
|
+
return Response([])
|
|
2487
|
+
|
|
2488
|
+
# Build query filters based on available fields
|
|
2489
|
+
# Check which fields exist on the User model
|
|
2490
|
+
user_fields = [f.name for f in User._meta.get_fields()]
|
|
2491
|
+
|
|
2492
|
+
filters = Q()
|
|
2493
|
+
if 'email' in user_fields:
|
|
2494
|
+
filters |= Q(email__icontains=query)
|
|
2495
|
+
if 'username' in user_fields:
|
|
2496
|
+
filters |= Q(username__icontains=query)
|
|
2497
|
+
if 'first_name' in user_fields:
|
|
2498
|
+
filters |= Q(first_name__icontains=query)
|
|
2499
|
+
if 'last_name' in user_fields:
|
|
2500
|
+
filters |= Q(last_name__icontains=query)
|
|
2501
|
+
if 'full_name' in user_fields:
|
|
2502
|
+
filters |= Q(full_name__icontains=query)
|
|
2503
|
+
|
|
2504
|
+
users = User.objects.filter(filters).exclude(
|
|
2505
|
+
id=request.user.id # Exclude current user
|
|
2506
|
+
)[:10]
|
|
2507
|
+
|
|
2508
|
+
results = []
|
|
2509
|
+
for user in users:
|
|
2510
|
+
# Get identifier (email or username)
|
|
2511
|
+
identifier = getattr(user, 'email', None) or getattr(user, 'username', None) or str(user)
|
|
2512
|
+
|
|
2513
|
+
# Get display name
|
|
2514
|
+
full_name = getattr(user, 'full_name', None) or ''
|
|
2515
|
+
first_name = getattr(user, 'first_name', None) or ''
|
|
2516
|
+
last_name = getattr(user, 'last_name', None) or ''
|
|
2517
|
+
name = full_name or f"{first_name} {last_name}".strip() or identifier
|
|
2518
|
+
|
|
2519
|
+
# Build display string
|
|
2520
|
+
if name != identifier:
|
|
2521
|
+
display = f"{name} ({identifier})"
|
|
2522
|
+
else:
|
|
2523
|
+
display = identifier
|
|
2524
|
+
|
|
2525
|
+
results.append({
|
|
2526
|
+
'id': str(user.id),
|
|
2527
|
+
'email': identifier, # Keep as 'email' for backward compatibility
|
|
2528
|
+
'name': name,
|
|
2529
|
+
'display': display,
|
|
2530
|
+
})
|
|
2531
|
+
|
|
2532
|
+
return Response(results)
|
|
@@ -976,17 +976,22 @@
|
|
|
976
976
|
max-height: none !important;
|
|
977
977
|
border-radius: 0;
|
|
978
978
|
box-shadow: none;
|
|
979
|
+
display: flex;
|
|
980
|
+
flex-direction: column;
|
|
981
|
+
overflow: hidden;
|
|
979
982
|
}
|
|
980
983
|
|
|
981
984
|
/* Embedded widget header can have rounded corners if desired */
|
|
982
985
|
.cw-widget-embedded .cw-header {
|
|
983
986
|
border-radius: 0;
|
|
987
|
+
flex-shrink: 0;
|
|
984
988
|
}
|
|
985
989
|
|
|
986
|
-
/* Ensure messages area fills available space */
|
|
990
|
+
/* Ensure messages area fills available space and scrolls */
|
|
987
991
|
.cw-widget-embedded .cw-messages {
|
|
988
992
|
flex: 1;
|
|
989
993
|
min-height: 0;
|
|
994
|
+
overflow-y: auto;
|
|
990
995
|
}
|
|
991
996
|
|
|
992
997
|
/* ============================================================================
|
|
@@ -58,11 +58,18 @@
|
|
|
58
58
|
</div>
|
|
59
59
|
</div>
|
|
60
60
|
<div class="border-t border-gray-100 px-4 py-3 bg-gray-50 flex items-center justify-between">
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
<div class="flex items-center space-x-3">
|
|
62
|
+
<a href="{% url 'agent_studio:agent_edit' agent.id %}"
|
|
63
|
+
class="text-primary-600 hover:text-primary-700 text-sm font-medium">
|
|
64
|
+
Edit
|
|
65
|
+
</a>
|
|
66
|
+
<a href="{% url 'agent_studio:agent_collaborators' agent.id %}"
|
|
67
|
+
class="text-gray-500 hover:text-gray-700 text-sm"
|
|
68
|
+
title="Manage collaborators">
|
|
69
|
+
<i class="pi pi-users"></i>
|
|
70
|
+
</a>
|
|
71
|
+
</div>
|
|
72
|
+
<a href="{% url 'agent_studio:agent_test' agent.id %}"
|
|
66
73
|
class="text-gray-600 hover:text-gray-700 text-sm">
|
|
67
74
|
Test →
|
|
68
75
|
</a>
|
|
@@ -100,6 +100,14 @@
|
|
|
100
100
|
height: 100% !important;
|
|
101
101
|
max-height: none !important;
|
|
102
102
|
border-radius: 0.5rem !important;
|
|
103
|
+
display: flex !important;
|
|
104
|
+
flex-direction: column !important;
|
|
105
|
+
overflow: hidden !important;
|
|
106
|
+
}
|
|
107
|
+
.embedded-chat .cw-messages {
|
|
108
|
+
flex: 1 !important;
|
|
109
|
+
min-height: 0 !important;
|
|
110
|
+
overflow-y: auto !important;
|
|
103
111
|
}
|
|
104
112
|
</style>
|
|
105
113
|
|
|
@@ -119,6 +127,10 @@
|
|
|
119
127
|
<div class="flex items-center space-x-4">
|
|
120
128
|
{% if request.user.is_authenticated %}
|
|
121
129
|
<span class="text-sm text-gray-600">{{ request.user.email|default:request.user }}</span>
|
|
130
|
+
<form method="post" action="{% url 'agent_studio:logout' %}" class="inline">
|
|
131
|
+
{% csrf_token %}
|
|
132
|
+
<button type="submit" class="text-sm text-gray-500 hover:text-gray-700">Logout</button>
|
|
133
|
+
</form>
|
|
122
134
|
{% endif %}
|
|
123
135
|
</div>
|
|
124
136
|
</header>
|