omgkit 2.1.1 → 2.2.0
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.
- package/package.json +1 -1
- package/plugin/skills/SKILL_STANDARDS.md +743 -0
- package/plugin/skills/databases/mongodb/SKILL.md +797 -28
- package/plugin/skills/databases/prisma/SKILL.md +776 -30
- package/plugin/skills/databases/redis/SKILL.md +885 -25
- package/plugin/skills/devops/aws/SKILL.md +686 -28
- package/plugin/skills/devops/github-actions/SKILL.md +684 -29
- package/plugin/skills/devops/kubernetes/SKILL.md +621 -24
- package/plugin/skills/frameworks/django/SKILL.md +920 -20
- package/plugin/skills/frameworks/express/SKILL.md +1361 -35
- package/plugin/skills/frameworks/fastapi/SKILL.md +1260 -33
- package/plugin/skills/frameworks/laravel/SKILL.md +1244 -31
- package/plugin/skills/frameworks/nestjs/SKILL.md +1005 -26
- package/plugin/skills/frameworks/rails/SKILL.md +594 -28
- package/plugin/skills/frameworks/spring/SKILL.md +528 -35
- package/plugin/skills/frameworks/vue/SKILL.md +1296 -27
- package/plugin/skills/frontend/accessibility/SKILL.md +1108 -34
- package/plugin/skills/frontend/frontend-design/SKILL.md +1304 -26
- package/plugin/skills/frontend/responsive/SKILL.md +847 -21
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +976 -38
- package/plugin/skills/frontend/tailwindcss/SKILL.md +831 -35
- package/plugin/skills/frontend/threejs/SKILL.md +1298 -29
- package/plugin/skills/languages/javascript/SKILL.md +935 -31
- package/plugin/skills/methodology/brainstorming/SKILL.md +597 -23
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +832 -34
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +665 -31
- package/plugin/skills/methodology/executing-plans/SKILL.md +556 -24
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +595 -25
- package/plugin/skills/methodology/problem-solving/SKILL.md +429 -61
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +536 -24
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +632 -21
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +641 -30
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +262 -3
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +571 -32
- package/plugin/skills/methodology/test-driven-development/SKILL.md +779 -24
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +691 -29
- package/plugin/skills/methodology/token-optimization/SKILL.md +598 -29
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +543 -22
- package/plugin/skills/methodology/writing-plans/SKILL.md +590 -18
- package/plugin/skills/omega/omega-architecture/SKILL.md +838 -39
- package/plugin/skills/omega/omega-coding/SKILL.md +636 -39
- package/plugin/skills/omega/omega-sprint/SKILL.md +855 -48
- package/plugin/skills/omega/omega-testing/SKILL.md +940 -41
- package/plugin/skills/omega/omega-thinking/SKILL.md +703 -50
- package/plugin/skills/security/better-auth/SKILL.md +1065 -28
- package/plugin/skills/security/oauth/SKILL.md +968 -31
- package/plugin/skills/security/owasp/SKILL.md +894 -33
- package/plugin/skills/testing/playwright/SKILL.md +764 -38
- package/plugin/skills/testing/pytest/SKILL.md +873 -36
- package/plugin/skills/testing/vitest/SKILL.md +980 -35
|
@@ -1,47 +1,947 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: django
|
|
3
|
-
description: Django development
|
|
3
|
+
description: Enterprise Django development with DRF, ORM optimization, async views, and production patterns
|
|
4
|
+
category: frameworks
|
|
5
|
+
triggers:
|
|
6
|
+
- django
|
|
7
|
+
- django rest framework
|
|
8
|
+
- drf
|
|
9
|
+
- django orm
|
|
10
|
+
- django admin
|
|
11
|
+
- django templates
|
|
12
|
+
- django views
|
|
13
|
+
- python web framework
|
|
4
14
|
---
|
|
5
15
|
|
|
6
|
-
# Django
|
|
16
|
+
# Django
|
|
7
17
|
|
|
8
|
-
|
|
18
|
+
Enterprise-grade **Django development** following industry best practices. This skill covers Django REST Framework, ORM optimization, async views, authentication, testing patterns, and production deployment configurations used by top engineering teams.
|
|
19
|
+
|
|
20
|
+
## Purpose
|
|
21
|
+
|
|
22
|
+
Build scalable Python web applications with confidence:
|
|
23
|
+
|
|
24
|
+
- Design clean model architectures with proper relationships
|
|
25
|
+
- Implement REST APIs with Django REST Framework
|
|
26
|
+
- Optimize database queries for performance
|
|
27
|
+
- Handle authentication and permissions securely
|
|
28
|
+
- Write comprehensive tests for reliability
|
|
29
|
+
- Deploy production-ready applications
|
|
30
|
+
- Leverage async views for high concurrency
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
### 1. Model Design and Relationships
|
|
9
35
|
|
|
10
|
-
### Model
|
|
11
36
|
```python
|
|
12
37
|
from django.db import models
|
|
38
|
+
from django.contrib.auth.models import AbstractUser
|
|
39
|
+
from django.utils import timezone
|
|
40
|
+
from uuid import uuid4
|
|
13
41
|
|
|
14
|
-
class User(
|
|
42
|
+
class User(AbstractUser):
|
|
43
|
+
"""Custom user model with UUID primary key and additional fields."""
|
|
44
|
+
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
|
15
45
|
email = models.EmailField(unique=True)
|
|
46
|
+
phone = models.CharField(max_length=20, blank=True)
|
|
47
|
+
avatar = models.ImageField(upload_to='avatars/', blank=True)
|
|
48
|
+
is_verified = models.BooleanField(default=False)
|
|
16
49
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
50
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
51
|
+
|
|
52
|
+
USERNAME_FIELD = 'email'
|
|
53
|
+
REQUIRED_FIELDS = ['username']
|
|
17
54
|
|
|
18
55
|
class Meta:
|
|
56
|
+
db_table = 'users'
|
|
19
57
|
ordering = ['-created_at']
|
|
20
|
-
|
|
58
|
+
indexes = [
|
|
59
|
+
models.Index(fields=['email']),
|
|
60
|
+
models.Index(fields=['created_at']),
|
|
61
|
+
]
|
|
21
62
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
63
|
+
def __str__(self):
|
|
64
|
+
return self.email
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Organization(models.Model):
|
|
68
|
+
"""Organization with membership relationships."""
|
|
69
|
+
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
|
70
|
+
name = models.CharField(max_length=255)
|
|
71
|
+
slug = models.SlugField(unique=True)
|
|
72
|
+
owner = models.ForeignKey(User, on_delete=models.PROTECT, related_name='owned_organizations')
|
|
73
|
+
members = models.ManyToManyField(User, through='Membership', related_name='organizations')
|
|
74
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
75
|
+
|
|
76
|
+
class Meta:
|
|
77
|
+
db_table = 'organizations'
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Membership(models.Model):
|
|
81
|
+
"""Through model for organization membership with roles."""
|
|
82
|
+
class Role(models.TextChoices):
|
|
83
|
+
OWNER = 'owner', 'Owner'
|
|
84
|
+
ADMIN = 'admin', 'Admin'
|
|
85
|
+
MEMBER = 'member', 'Member'
|
|
86
|
+
VIEWER = 'viewer', 'Viewer'
|
|
87
|
+
|
|
88
|
+
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
|
89
|
+
organization = models.ForeignKey(Organization, on_delete=models.CASCADE)
|
|
90
|
+
role = models.CharField(max_length=20, choices=Role.choices, default=Role.MEMBER)
|
|
91
|
+
joined_at = models.DateTimeField(auto_now_add=True)
|
|
92
|
+
|
|
93
|
+
class Meta:
|
|
94
|
+
db_table = 'memberships'
|
|
95
|
+
unique_together = ['user', 'organization']
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class Project(models.Model):
|
|
99
|
+
"""Project with soft delete and audit fields."""
|
|
100
|
+
class Status(models.TextChoices):
|
|
101
|
+
DRAFT = 'draft', 'Draft'
|
|
102
|
+
ACTIVE = 'active', 'Active'
|
|
103
|
+
ARCHIVED = 'archived', 'Archived'
|
|
104
|
+
|
|
105
|
+
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
|
106
|
+
organization = models.ForeignKey(Organization, on_delete=models.CASCADE, related_name='projects')
|
|
107
|
+
name = models.CharField(max_length=255)
|
|
108
|
+
description = models.TextField(blank=True)
|
|
109
|
+
status = models.CharField(max_length=20, choices=Status.choices, default=Status.DRAFT)
|
|
110
|
+
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
|
|
111
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
112
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
113
|
+
deleted_at = models.DateTimeField(null=True, blank=True)
|
|
114
|
+
|
|
115
|
+
class Meta:
|
|
116
|
+
db_table = 'projects'
|
|
117
|
+
ordering = ['-created_at']
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def is_deleted(self):
|
|
121
|
+
return self.deleted_at is not None
|
|
26
122
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return JsonResponse({'email': user.email})
|
|
123
|
+
def soft_delete(self):
|
|
124
|
+
self.deleted_at = timezone.now()
|
|
125
|
+
self.save(update_fields=['deleted_at'])
|
|
31
126
|
```
|
|
32
127
|
|
|
33
|
-
###
|
|
128
|
+
### 2. Django REST Framework Serializers
|
|
129
|
+
|
|
34
130
|
```python
|
|
35
131
|
from rest_framework import serializers
|
|
132
|
+
from django.contrib.auth import get_user_model
|
|
133
|
+
from django.contrib.auth.password_validation import validate_password
|
|
134
|
+
|
|
135
|
+
User = get_user_model()
|
|
136
|
+
|
|
36
137
|
|
|
37
138
|
class UserSerializer(serializers.ModelSerializer):
|
|
139
|
+
"""User serializer with computed fields."""
|
|
140
|
+
full_name = serializers.SerializerMethodField()
|
|
141
|
+
organization_count = serializers.SerializerMethodField()
|
|
142
|
+
|
|
143
|
+
class Meta:
|
|
144
|
+
model = User
|
|
145
|
+
fields = ['id', 'email', 'username', 'full_name', 'avatar',
|
|
146
|
+
'is_verified', 'organization_count', 'created_at']
|
|
147
|
+
read_only_fields = ['id', 'is_verified', 'created_at']
|
|
148
|
+
|
|
149
|
+
def get_full_name(self, obj):
|
|
150
|
+
return f"{obj.first_name} {obj.last_name}".strip() or obj.username
|
|
151
|
+
|
|
152
|
+
def get_organization_count(self, obj):
|
|
153
|
+
return obj.organizations.count()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class UserCreateSerializer(serializers.ModelSerializer):
|
|
157
|
+
"""User registration serializer with password validation."""
|
|
158
|
+
password = serializers.CharField(write_only=True, validators=[validate_password])
|
|
159
|
+
password_confirm = serializers.CharField(write_only=True)
|
|
160
|
+
|
|
38
161
|
class Meta:
|
|
39
162
|
model = User
|
|
40
|
-
fields = ['
|
|
163
|
+
fields = ['email', 'username', 'password', 'password_confirm']
|
|
164
|
+
|
|
165
|
+
def validate(self, attrs):
|
|
166
|
+
if attrs['password'] != attrs['password_confirm']:
|
|
167
|
+
raise serializers.ValidationError({'password_confirm': 'Passwords do not match'})
|
|
168
|
+
return attrs
|
|
169
|
+
|
|
170
|
+
def create(self, validated_data):
|
|
171
|
+
validated_data.pop('password_confirm')
|
|
172
|
+
password = validated_data.pop('password')
|
|
173
|
+
user = User(**validated_data)
|
|
174
|
+
user.set_password(password)
|
|
175
|
+
user.save()
|
|
176
|
+
return user
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class OrganizationSerializer(serializers.ModelSerializer):
|
|
180
|
+
"""Organization serializer with nested relationships."""
|
|
181
|
+
owner = UserSerializer(read_only=True)
|
|
182
|
+
member_count = serializers.SerializerMethodField()
|
|
183
|
+
|
|
184
|
+
class Meta:
|
|
185
|
+
model = Organization
|
|
186
|
+
fields = ['id', 'name', 'slug', 'owner', 'member_count', 'created_at']
|
|
187
|
+
read_only_fields = ['id', 'owner', 'created_at']
|
|
188
|
+
|
|
189
|
+
def get_member_count(self, obj):
|
|
190
|
+
return obj.members.count()
|
|
191
|
+
|
|
192
|
+
def create(self, validated_data):
|
|
193
|
+
user = self.context['request'].user
|
|
194
|
+
org = Organization.objects.create(owner=user, **validated_data)
|
|
195
|
+
Membership.objects.create(user=user, organization=org, role=Membership.Role.OWNER)
|
|
196
|
+
return org
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class ProjectSerializer(serializers.ModelSerializer):
|
|
200
|
+
"""Project serializer with validation."""
|
|
201
|
+
created_by = UserSerializer(read_only=True)
|
|
202
|
+
|
|
203
|
+
class Meta:
|
|
204
|
+
model = Project
|
|
205
|
+
fields = ['id', 'name', 'description', 'status', 'created_by',
|
|
206
|
+
'created_at', 'updated_at']
|
|
207
|
+
read_only_fields = ['id', 'created_by', 'created_at', 'updated_at']
|
|
208
|
+
|
|
209
|
+
def validate_name(self, value):
|
|
210
|
+
org = self.context.get('organization')
|
|
211
|
+
if org and Project.objects.filter(organization=org, name=value).exists():
|
|
212
|
+
raise serializers.ValidationError('Project with this name already exists')
|
|
213
|
+
return value
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### 3. Views and ViewSets
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from rest_framework import viewsets, status, permissions
|
|
220
|
+
from rest_framework.decorators import action
|
|
221
|
+
from rest_framework.response import Response
|
|
222
|
+
from rest_framework.views import APIView
|
|
223
|
+
from django.shortcuts import get_object_or_404
|
|
224
|
+
from django.db.models import Q, Count, Prefetch
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class UserViewSet(viewsets.ModelViewSet):
|
|
228
|
+
"""User management viewset with custom actions."""
|
|
229
|
+
queryset = User.objects.all()
|
|
230
|
+
permission_classes = [permissions.IsAuthenticated]
|
|
231
|
+
|
|
232
|
+
def get_serializer_class(self):
|
|
233
|
+
if self.action == 'create':
|
|
234
|
+
return UserCreateSerializer
|
|
235
|
+
return UserSerializer
|
|
236
|
+
|
|
237
|
+
def get_queryset(self):
|
|
238
|
+
queryset = super().get_queryset()
|
|
239
|
+
# Optimize with prefetch
|
|
240
|
+
return queryset.prefetch_related(
|
|
241
|
+
Prefetch('organizations', queryset=Organization.objects.only('id', 'name'))
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
@action(detail=False, methods=['get', 'patch'])
|
|
245
|
+
def me(self, request):
|
|
246
|
+
"""Get or update current user."""
|
|
247
|
+
if request.method == 'GET':
|
|
248
|
+
serializer = self.get_serializer(request.user)
|
|
249
|
+
return Response(serializer.data)
|
|
250
|
+
|
|
251
|
+
serializer = self.get_serializer(request.user, data=request.data, partial=True)
|
|
252
|
+
serializer.is_valid(raise_exception=True)
|
|
253
|
+
serializer.save()
|
|
254
|
+
return Response(serializer.data)
|
|
255
|
+
|
|
256
|
+
@action(detail=True, methods=['post'])
|
|
257
|
+
def verify(self, request, pk=None):
|
|
258
|
+
"""Admin action to verify a user."""
|
|
259
|
+
if not request.user.is_staff:
|
|
260
|
+
return Response({'error': 'Admin only'}, status=status.HTTP_403_FORBIDDEN)
|
|
261
|
+
|
|
262
|
+
user = self.get_object()
|
|
263
|
+
user.is_verified = True
|
|
264
|
+
user.save(update_fields=['is_verified'])
|
|
265
|
+
return Response({'status': 'verified'})
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class OrganizationViewSet(viewsets.ModelViewSet):
|
|
269
|
+
"""Organization viewset with membership management."""
|
|
270
|
+
serializer_class = OrganizationSerializer
|
|
271
|
+
permission_classes = [permissions.IsAuthenticated]
|
|
272
|
+
lookup_field = 'slug'
|
|
273
|
+
|
|
274
|
+
def get_queryset(self):
|
|
275
|
+
return Organization.objects.filter(
|
|
276
|
+
members=self.request.user
|
|
277
|
+
).select_related('owner').annotate(
|
|
278
|
+
member_count=Count('members')
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
@action(detail=True, methods=['get'])
|
|
282
|
+
def members(self, request, slug=None):
|
|
283
|
+
"""List organization members."""
|
|
284
|
+
org = self.get_object()
|
|
285
|
+
memberships = Membership.objects.filter(organization=org).select_related('user')
|
|
286
|
+
data = [
|
|
287
|
+
{
|
|
288
|
+
'user': UserSerializer(m.user).data,
|
|
289
|
+
'role': m.role,
|
|
290
|
+
'joined_at': m.joined_at
|
|
291
|
+
}
|
|
292
|
+
for m in memberships
|
|
293
|
+
]
|
|
294
|
+
return Response(data)
|
|
295
|
+
|
|
296
|
+
@action(detail=True, methods=['post'])
|
|
297
|
+
def invite(self, request, slug=None):
|
|
298
|
+
"""Invite a user to the organization."""
|
|
299
|
+
org = self.get_object()
|
|
300
|
+
email = request.data.get('email')
|
|
301
|
+
role = request.data.get('role', Membership.Role.MEMBER)
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
user = User.objects.get(email=email)
|
|
305
|
+
except User.DoesNotExist:
|
|
306
|
+
return Response({'error': 'User not found'}, status=status.HTTP_404_NOT_FOUND)
|
|
307
|
+
|
|
308
|
+
membership, created = Membership.objects.get_or_create(
|
|
309
|
+
user=user, organization=org,
|
|
310
|
+
defaults={'role': role}
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
if not created:
|
|
314
|
+
return Response({'error': 'User already a member'}, status=status.HTTP_400_BAD_REQUEST)
|
|
315
|
+
|
|
316
|
+
return Response({'status': 'invited'}, status=status.HTTP_201_CREATED)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class ProjectViewSet(viewsets.ModelViewSet):
|
|
320
|
+
"""Project viewset scoped to organization."""
|
|
321
|
+
serializer_class = ProjectSerializer
|
|
322
|
+
permission_classes = [permissions.IsAuthenticated]
|
|
323
|
+
|
|
324
|
+
def get_queryset(self):
|
|
325
|
+
org_slug = self.kwargs.get('org_slug')
|
|
326
|
+
return Project.objects.filter(
|
|
327
|
+
organization__slug=org_slug,
|
|
328
|
+
organization__members=self.request.user,
|
|
329
|
+
deleted_at__isnull=True
|
|
330
|
+
).select_related('created_by')
|
|
331
|
+
|
|
332
|
+
def get_serializer_context(self):
|
|
333
|
+
context = super().get_serializer_context()
|
|
334
|
+
org_slug = self.kwargs.get('org_slug')
|
|
335
|
+
context['organization'] = get_object_or_404(Organization, slug=org_slug)
|
|
336
|
+
return context
|
|
337
|
+
|
|
338
|
+
def perform_create(self, serializer):
|
|
339
|
+
org_slug = self.kwargs.get('org_slug')
|
|
340
|
+
org = get_object_or_404(Organization, slug=org_slug)
|
|
341
|
+
serializer.save(organization=org, created_by=self.request.user)
|
|
342
|
+
|
|
343
|
+
def perform_destroy(self, instance):
|
|
344
|
+
# Soft delete instead of hard delete
|
|
345
|
+
instance.soft_delete()
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### 4. Custom Permissions and Authentication
|
|
349
|
+
|
|
350
|
+
```python
|
|
351
|
+
from rest_framework import permissions
|
|
352
|
+
from rest_framework.authentication import TokenAuthentication
|
|
353
|
+
from rest_framework_simplejwt.authentication import JWTAuthentication
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class IsOrganizationMember(permissions.BasePermission):
|
|
357
|
+
"""Check if user is a member of the organization."""
|
|
358
|
+
|
|
359
|
+
def has_permission(self, request, view):
|
|
360
|
+
org_slug = view.kwargs.get('org_slug')
|
|
361
|
+
if not org_slug:
|
|
362
|
+
return True
|
|
363
|
+
return Membership.objects.filter(
|
|
364
|
+
user=request.user,
|
|
365
|
+
organization__slug=org_slug
|
|
366
|
+
).exists()
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
class IsOrganizationAdmin(permissions.BasePermission):
|
|
370
|
+
"""Check if user is an admin of the organization."""
|
|
371
|
+
|
|
372
|
+
def has_permission(self, request, view):
|
|
373
|
+
org_slug = view.kwargs.get('org_slug')
|
|
374
|
+
if not org_slug:
|
|
375
|
+
return False
|
|
376
|
+
return Membership.objects.filter(
|
|
377
|
+
user=request.user,
|
|
378
|
+
organization__slug=org_slug,
|
|
379
|
+
role__in=[Membership.Role.OWNER, Membership.Role.ADMIN]
|
|
380
|
+
).exists()
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class IsOwnerOrReadOnly(permissions.BasePermission):
|
|
384
|
+
"""Object-level permission for owner access."""
|
|
385
|
+
|
|
386
|
+
def has_object_permission(self, request, view, obj):
|
|
387
|
+
if request.method in permissions.SAFE_METHODS:
|
|
388
|
+
return True
|
|
389
|
+
|
|
390
|
+
# Check various ownership patterns
|
|
391
|
+
if hasattr(obj, 'owner'):
|
|
392
|
+
return obj.owner == request.user
|
|
393
|
+
if hasattr(obj, 'created_by'):
|
|
394
|
+
return obj.created_by == request.user
|
|
395
|
+
if hasattr(obj, 'user'):
|
|
396
|
+
return obj.user == request.user
|
|
397
|
+
|
|
398
|
+
return False
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
# Custom JWT Authentication with additional claims
|
|
402
|
+
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
|
|
403
|
+
from rest_framework_simplejwt.views import TokenObtainPairView
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
|
|
407
|
+
"""Add custom claims to JWT token."""
|
|
408
|
+
|
|
409
|
+
@classmethod
|
|
410
|
+
def get_token(cls, user):
|
|
411
|
+
token = super().get_token(user)
|
|
412
|
+
token['email'] = user.email
|
|
413
|
+
token['is_verified'] = user.is_verified
|
|
414
|
+
token['is_staff'] = user.is_staff
|
|
415
|
+
return token
|
|
416
|
+
|
|
417
|
+
def validate(self, attrs):
|
|
418
|
+
data = super().validate(attrs)
|
|
419
|
+
data['user'] = UserSerializer(self.user).data
|
|
420
|
+
return data
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
class CustomTokenObtainPairView(TokenObtainPairView):
|
|
424
|
+
serializer_class = CustomTokenObtainPairSerializer
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### 5. Query Optimization and Managers
|
|
428
|
+
|
|
429
|
+
```python
|
|
430
|
+
from django.db import models
|
|
431
|
+
from django.db.models import Q, Count, Avg, F, Prefetch
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
class ProjectManager(models.Manager):
|
|
435
|
+
"""Custom manager with optimized queries."""
|
|
436
|
+
|
|
437
|
+
def get_queryset(self):
|
|
438
|
+
return super().get_queryset().filter(deleted_at__isnull=True)
|
|
439
|
+
|
|
440
|
+
def with_stats(self):
|
|
441
|
+
"""Include task statistics."""
|
|
442
|
+
return self.annotate(
|
|
443
|
+
task_count=Count('tasks'),
|
|
444
|
+
completed_task_count=Count('tasks', filter=Q(tasks__status='completed')),
|
|
445
|
+
completion_rate=F('completed_task_count') * 100.0 / F('task_count')
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
def for_user(self, user):
|
|
449
|
+
"""Filter projects accessible to user."""
|
|
450
|
+
return self.filter(organization__members=user)
|
|
451
|
+
|
|
452
|
+
def active(self):
|
|
453
|
+
"""Filter active projects."""
|
|
454
|
+
return self.filter(status=Project.Status.ACTIVE)
|
|
455
|
+
|
|
456
|
+
def with_recent_activity(self, days=7):
|
|
457
|
+
"""Filter projects with recent activity."""
|
|
458
|
+
from datetime import timedelta
|
|
459
|
+
from django.utils import timezone
|
|
460
|
+
cutoff = timezone.now() - timedelta(days=days)
|
|
461
|
+
return self.filter(
|
|
462
|
+
Q(updated_at__gte=cutoff) | Q(tasks__updated_at__gte=cutoff)
|
|
463
|
+
).distinct()
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
# Optimized queryset usage in views
|
|
467
|
+
class OptimizedProjectViewSet(viewsets.ModelViewSet):
|
|
468
|
+
"""ViewSet demonstrating query optimization."""
|
|
469
|
+
|
|
470
|
+
def get_queryset(self):
|
|
471
|
+
return Project.objects.select_related(
|
|
472
|
+
'organization',
|
|
473
|
+
'created_by'
|
|
474
|
+
).prefetch_related(
|
|
475
|
+
Prefetch(
|
|
476
|
+
'tasks',
|
|
477
|
+
queryset=Task.objects.filter(status='active').only('id', 'title', 'status')
|
|
478
|
+
),
|
|
479
|
+
'organization__members'
|
|
480
|
+
).with_stats().for_user(self.request.user)
|
|
481
|
+
|
|
482
|
+
def list(self, request, *args, **kwargs):
|
|
483
|
+
# Use values() for lightweight list responses
|
|
484
|
+
queryset = self.get_queryset().values(
|
|
485
|
+
'id', 'name', 'status', 'task_count', 'completion_rate', 'created_at'
|
|
486
|
+
)
|
|
487
|
+
return Response(list(queryset))
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### 6. Async Views and Background Tasks
|
|
491
|
+
|
|
492
|
+
```python
|
|
493
|
+
from django.http import JsonResponse
|
|
494
|
+
from django.views import View
|
|
495
|
+
from asgiref.sync import sync_to_async
|
|
496
|
+
import asyncio
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
class AsyncProjectView(View):
|
|
500
|
+
"""Async view for concurrent operations."""
|
|
501
|
+
|
|
502
|
+
async def get(self, request, project_id):
|
|
503
|
+
# Run multiple async operations concurrently
|
|
504
|
+
project, tasks, activity = await asyncio.gather(
|
|
505
|
+
self.get_project(project_id),
|
|
506
|
+
self.get_tasks(project_id),
|
|
507
|
+
self.get_recent_activity(project_id)
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
return JsonResponse({
|
|
511
|
+
'project': project,
|
|
512
|
+
'tasks': tasks,
|
|
513
|
+
'activity': activity
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
@sync_to_async
|
|
517
|
+
def get_project(self, project_id):
|
|
518
|
+
project = Project.objects.select_related('created_by').get(id=project_id)
|
|
519
|
+
return {
|
|
520
|
+
'id': str(project.id),
|
|
521
|
+
'name': project.name,
|
|
522
|
+
'status': project.status,
|
|
523
|
+
'created_by': project.created_by.email
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
@sync_to_async
|
|
527
|
+
def get_tasks(self, project_id):
|
|
528
|
+
tasks = list(Task.objects.filter(project_id=project_id).values('id', 'title', 'status'))
|
|
529
|
+
return tasks
|
|
530
|
+
|
|
531
|
+
@sync_to_async
|
|
532
|
+
def get_recent_activity(self, project_id):
|
|
533
|
+
# Fetch recent activity logs
|
|
534
|
+
from datetime import timedelta
|
|
535
|
+
from django.utils import timezone
|
|
536
|
+
cutoff = timezone.now() - timedelta(days=7)
|
|
537
|
+
activities = list(ActivityLog.objects.filter(
|
|
538
|
+
project_id=project_id,
|
|
539
|
+
created_at__gte=cutoff
|
|
540
|
+
).values('action', 'created_at')[:10])
|
|
541
|
+
return activities
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
# Celery background tasks
|
|
545
|
+
from celery import shared_task
|
|
546
|
+
from django.core.mail import send_mail
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
@shared_task(bind=True, max_retries=3)
|
|
550
|
+
def send_invitation_email(self, user_id, org_id):
|
|
551
|
+
"""Send organization invitation email."""
|
|
552
|
+
try:
|
|
553
|
+
user = User.objects.get(id=user_id)
|
|
554
|
+
org = Organization.objects.get(id=org_id)
|
|
555
|
+
|
|
556
|
+
send_mail(
|
|
557
|
+
subject=f'Invitation to join {org.name}',
|
|
558
|
+
message=f'You have been invited to join {org.name}.',
|
|
559
|
+
from_email='noreply@example.com',
|
|
560
|
+
recipient_list=[user.email],
|
|
561
|
+
fail_silently=False,
|
|
562
|
+
)
|
|
563
|
+
except Exception as exc:
|
|
564
|
+
raise self.retry(exc=exc, countdown=60)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
@shared_task
|
|
568
|
+
def generate_project_report(project_id):
|
|
569
|
+
"""Generate project report asynchronously."""
|
|
570
|
+
project = Project.objects.prefetch_related('tasks').get(id=project_id)
|
|
571
|
+
|
|
572
|
+
report_data = {
|
|
573
|
+
'project': project.name,
|
|
574
|
+
'total_tasks': project.tasks.count(),
|
|
575
|
+
'completed_tasks': project.tasks.filter(status='completed').count(),
|
|
576
|
+
'generated_at': timezone.now().isoformat()
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
# Save report to storage
|
|
580
|
+
from django.core.files.base import ContentFile
|
|
581
|
+
import json
|
|
582
|
+
|
|
583
|
+
report_content = json.dumps(report_data, indent=2)
|
|
584
|
+
project.latest_report.save(
|
|
585
|
+
f'report_{project_id}.json',
|
|
586
|
+
ContentFile(report_content.encode())
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
return report_data
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
### 7. Testing Patterns
|
|
593
|
+
|
|
594
|
+
```python
|
|
595
|
+
import pytest
|
|
596
|
+
from django.urls import reverse
|
|
597
|
+
from rest_framework import status
|
|
598
|
+
from rest_framework.test import APITestCase
|
|
599
|
+
from model_bakery import baker
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
class UserAPITestCase(APITestCase):
|
|
603
|
+
"""Test case for user API endpoints."""
|
|
604
|
+
|
|
605
|
+
def setUp(self):
|
|
606
|
+
self.user = baker.make(User, email='test@example.com')
|
|
607
|
+
self.client.force_authenticate(user=self.user)
|
|
608
|
+
|
|
609
|
+
def test_get_current_user(self):
|
|
610
|
+
"""Test retrieving current user profile."""
|
|
611
|
+
url = reverse('user-me')
|
|
612
|
+
response = self.client.get(url)
|
|
613
|
+
|
|
614
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
615
|
+
self.assertEqual(response.data['email'], 'test@example.com')
|
|
616
|
+
|
|
617
|
+
def test_update_current_user(self):
|
|
618
|
+
"""Test updating current user profile."""
|
|
619
|
+
url = reverse('user-me')
|
|
620
|
+
response = self.client.patch(url, {'username': 'newusername'})
|
|
621
|
+
|
|
622
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
623
|
+
self.user.refresh_from_db()
|
|
624
|
+
self.assertEqual(self.user.username, 'newusername')
|
|
625
|
+
|
|
626
|
+
def test_create_user_with_weak_password(self):
|
|
627
|
+
"""Test user creation fails with weak password."""
|
|
628
|
+
url = reverse('user-list')
|
|
629
|
+
data = {
|
|
630
|
+
'email': 'new@example.com',
|
|
631
|
+
'username': 'newuser',
|
|
632
|
+
'password': '123',
|
|
633
|
+
'password_confirm': '123'
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
self.client.force_authenticate(user=None)
|
|
637
|
+
response = self.client.post(url, data)
|
|
638
|
+
|
|
639
|
+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
640
|
+
self.assertIn('password', response.data)
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
class OrganizationAPITestCase(APITestCase):
|
|
644
|
+
"""Test case for organization API endpoints."""
|
|
645
|
+
|
|
646
|
+
def setUp(self):
|
|
647
|
+
self.user = baker.make(User)
|
|
648
|
+
self.org = baker.make(Organization, owner=self.user)
|
|
649
|
+
baker.make(Membership, user=self.user, organization=self.org, role=Membership.Role.OWNER)
|
|
650
|
+
self.client.force_authenticate(user=self.user)
|
|
651
|
+
|
|
652
|
+
def test_list_user_organizations(self):
|
|
653
|
+
"""Test listing organizations user belongs to."""
|
|
654
|
+
# Create another org user is not part of
|
|
655
|
+
other_org = baker.make(Organization)
|
|
656
|
+
|
|
657
|
+
url = reverse('organization-list')
|
|
658
|
+
response = self.client.get(url)
|
|
659
|
+
|
|
660
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
661
|
+
self.assertEqual(len(response.data), 1)
|
|
662
|
+
self.assertEqual(response.data[0]['slug'], self.org.slug)
|
|
663
|
+
|
|
664
|
+
def test_create_organization(self):
|
|
665
|
+
"""Test creating a new organization."""
|
|
666
|
+
url = reverse('organization-list')
|
|
667
|
+
data = {'name': 'New Org', 'slug': 'new-org'}
|
|
668
|
+
|
|
669
|
+
response = self.client.post(url, data)
|
|
670
|
+
|
|
671
|
+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
672
|
+
self.assertEqual(response.data['name'], 'New Org')
|
|
673
|
+
# Verify membership was created
|
|
674
|
+
self.assertTrue(Membership.objects.filter(
|
|
675
|
+
user=self.user,
|
|
676
|
+
organization__slug='new-org',
|
|
677
|
+
role=Membership.Role.OWNER
|
|
678
|
+
).exists())
|
|
679
|
+
|
|
680
|
+
def test_invite_member(self):
|
|
681
|
+
"""Test inviting a member to organization."""
|
|
682
|
+
new_user = baker.make(User, email='invite@example.com')
|
|
683
|
+
url = reverse('organization-invite', kwargs={'slug': self.org.slug})
|
|
684
|
+
|
|
685
|
+
response = self.client.post(url, {'email': 'invite@example.com', 'role': 'member'})
|
|
686
|
+
|
|
687
|
+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
688
|
+
self.assertTrue(Membership.objects.filter(
|
|
689
|
+
user=new_user,
|
|
690
|
+
organization=self.org
|
|
691
|
+
).exists())
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
@pytest.fixture
|
|
695
|
+
def api_client():
|
|
696
|
+
"""Pytest fixture for API client."""
|
|
697
|
+
from rest_framework.test import APIClient
|
|
698
|
+
return APIClient()
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
@pytest.fixture
|
|
702
|
+
def authenticated_client(api_client):
|
|
703
|
+
"""Pytest fixture for authenticated API client."""
|
|
704
|
+
user = baker.make(User)
|
|
705
|
+
api_client.force_authenticate(user=user)
|
|
706
|
+
api_client.user = user
|
|
707
|
+
return api_client
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
@pytest.mark.django_db
|
|
711
|
+
class TestProjectAPI:
|
|
712
|
+
"""Pytest-based tests for project API."""
|
|
713
|
+
|
|
714
|
+
def test_create_project(self, authenticated_client):
|
|
715
|
+
org = baker.make(Organization, owner=authenticated_client.user)
|
|
716
|
+
baker.make(Membership, user=authenticated_client.user, organization=org)
|
|
717
|
+
|
|
718
|
+
url = reverse('project-list', kwargs={'org_slug': org.slug})
|
|
719
|
+
response = authenticated_client.post(url, {
|
|
720
|
+
'name': 'Test Project',
|
|
721
|
+
'description': 'A test project'
|
|
722
|
+
})
|
|
723
|
+
|
|
724
|
+
assert response.status_code == status.HTTP_201_CREATED
|
|
725
|
+
assert response.data['name'] == 'Test Project'
|
|
726
|
+
assert response.data['created_by']['id'] == str(authenticated_client.user.id)
|
|
727
|
+
|
|
728
|
+
def test_soft_delete_project(self, authenticated_client):
|
|
729
|
+
org = baker.make(Organization, owner=authenticated_client.user)
|
|
730
|
+
baker.make(Membership, user=authenticated_client.user, organization=org)
|
|
731
|
+
project = baker.make(Project, organization=org, created_by=authenticated_client.user)
|
|
732
|
+
|
|
733
|
+
url = reverse('project-detail', kwargs={
|
|
734
|
+
'org_slug': org.slug,
|
|
735
|
+
'pk': project.id
|
|
736
|
+
})
|
|
737
|
+
response = authenticated_client.delete(url)
|
|
738
|
+
|
|
739
|
+
assert response.status_code == status.HTTP_204_NO_CONTENT
|
|
740
|
+
project.refresh_from_db()
|
|
741
|
+
assert project.deleted_at is not None
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
## Use Cases
|
|
745
|
+
|
|
746
|
+
### Multi-tenant SaaS Application
|
|
747
|
+
|
|
748
|
+
```python
|
|
749
|
+
# settings.py - Multi-tenant configuration
|
|
750
|
+
MIDDLEWARE = [
|
|
751
|
+
'django.middleware.security.SecurityMiddleware',
|
|
752
|
+
'django.contrib.sessions.middleware.SessionMiddleware',
|
|
753
|
+
'apps.tenants.middleware.TenantMiddleware', # Custom tenant middleware
|
|
754
|
+
'django.middleware.common.CommonMiddleware',
|
|
755
|
+
# ...
|
|
756
|
+
]
|
|
757
|
+
|
|
758
|
+
# middleware.py
|
|
759
|
+
from django.utils.deprecation import MiddlewareMixin
|
|
760
|
+
from threading import local
|
|
761
|
+
|
|
762
|
+
_thread_locals = local()
|
|
763
|
+
|
|
764
|
+
def get_current_tenant():
|
|
765
|
+
return getattr(_thread_locals, 'tenant', None)
|
|
766
|
+
|
|
767
|
+
class TenantMiddleware(MiddlewareMixin):
|
|
768
|
+
def process_request(self, request):
|
|
769
|
+
# Extract tenant from subdomain or header
|
|
770
|
+
host = request.get_host().split(':')[0]
|
|
771
|
+
subdomain = host.split('.')[0]
|
|
772
|
+
|
|
773
|
+
try:
|
|
774
|
+
tenant = Organization.objects.get(slug=subdomain)
|
|
775
|
+
_thread_locals.tenant = tenant
|
|
776
|
+
request.tenant = tenant
|
|
777
|
+
except Organization.DoesNotExist:
|
|
778
|
+
_thread_locals.tenant = None
|
|
779
|
+
request.tenant = None
|
|
780
|
+
|
|
781
|
+
# managers.py - Tenant-aware manager
|
|
782
|
+
class TenantManager(models.Manager):
|
|
783
|
+
def get_queryset(self):
|
|
784
|
+
tenant = get_current_tenant()
|
|
785
|
+
qs = super().get_queryset()
|
|
786
|
+
if tenant:
|
|
787
|
+
return qs.filter(organization=tenant)
|
|
788
|
+
return qs
|
|
789
|
+
|
|
790
|
+
# models.py - Tenant-scoped model
|
|
791
|
+
class TenantModel(models.Model):
|
|
792
|
+
organization = models.ForeignKey(Organization, on_delete=models.CASCADE)
|
|
793
|
+
|
|
794
|
+
objects = TenantManager()
|
|
795
|
+
all_objects = models.Manager() # Bypass tenant filtering
|
|
796
|
+
|
|
797
|
+
class Meta:
|
|
798
|
+
abstract = True
|
|
799
|
+
|
|
800
|
+
def save(self, *args, **kwargs):
|
|
801
|
+
if not self.organization_id:
|
|
802
|
+
self.organization = get_current_tenant()
|
|
803
|
+
super().save(*args, **kwargs)
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
### Real-time Dashboard with WebSockets
|
|
807
|
+
|
|
808
|
+
```python
|
|
809
|
+
# consumers.py - Django Channels WebSocket consumer
|
|
810
|
+
import json
|
|
811
|
+
from channels.generic.websocket import AsyncWebsocketConsumer
|
|
812
|
+
from channels.db import database_sync_to_async
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
class DashboardConsumer(AsyncWebsocketConsumer):
|
|
816
|
+
async def connect(self):
|
|
817
|
+
self.user = self.scope['user']
|
|
818
|
+
if not self.user.is_authenticated:
|
|
819
|
+
await self.close()
|
|
820
|
+
return
|
|
821
|
+
|
|
822
|
+
self.project_id = self.scope['url_route']['kwargs']['project_id']
|
|
823
|
+
self.room_group_name = f'dashboard_{self.project_id}'
|
|
824
|
+
|
|
825
|
+
# Verify user has access to project
|
|
826
|
+
if not await self.has_project_access():
|
|
827
|
+
await self.close()
|
|
828
|
+
return
|
|
829
|
+
|
|
830
|
+
# Join room group
|
|
831
|
+
await self.channel_layer.group_add(
|
|
832
|
+
self.room_group_name,
|
|
833
|
+
self.channel_name
|
|
834
|
+
)
|
|
835
|
+
await self.accept()
|
|
836
|
+
|
|
837
|
+
# Send initial data
|
|
838
|
+
await self.send_dashboard_data()
|
|
839
|
+
|
|
840
|
+
async def disconnect(self, close_code):
|
|
841
|
+
await self.channel_layer.group_discard(
|
|
842
|
+
self.room_group_name,
|
|
843
|
+
self.channel_name
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
async def receive(self, text_data):
|
|
847
|
+
data = json.loads(text_data)
|
|
848
|
+
action = data.get('action')
|
|
849
|
+
|
|
850
|
+
if action == 'refresh':
|
|
851
|
+
await self.send_dashboard_data()
|
|
852
|
+
elif action == 'subscribe':
|
|
853
|
+
# Handle subscription to specific metrics
|
|
854
|
+
pass
|
|
855
|
+
|
|
856
|
+
async def dashboard_update(self, event):
|
|
857
|
+
"""Handler for dashboard update messages."""
|
|
858
|
+
await self.send(text_data=json.dumps({
|
|
859
|
+
'type': 'update',
|
|
860
|
+
'data': event['data']
|
|
861
|
+
}))
|
|
862
|
+
|
|
863
|
+
@database_sync_to_async
|
|
864
|
+
def has_project_access(self):
|
|
865
|
+
return Project.objects.filter(
|
|
866
|
+
id=self.project_id,
|
|
867
|
+
organization__members=self.user
|
|
868
|
+
).exists()
|
|
869
|
+
|
|
870
|
+
@database_sync_to_async
|
|
871
|
+
def get_dashboard_data(self):
|
|
872
|
+
project = Project.objects.prefetch_related('tasks').get(id=self.project_id)
|
|
873
|
+
return {
|
|
874
|
+
'project': {'id': str(project.id), 'name': project.name},
|
|
875
|
+
'stats': {
|
|
876
|
+
'total_tasks': project.tasks.count(),
|
|
877
|
+
'completed': project.tasks.filter(status='completed').count(),
|
|
878
|
+
'in_progress': project.tasks.filter(status='in_progress').count(),
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
async def send_dashboard_data(self):
|
|
883
|
+
data = await self.get_dashboard_data()
|
|
884
|
+
await self.send(text_data=json.dumps({
|
|
885
|
+
'type': 'initial',
|
|
886
|
+
'data': data
|
|
887
|
+
}))
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
# Signal to broadcast updates
|
|
891
|
+
from django.db.models.signals import post_save
|
|
892
|
+
from django.dispatch import receiver
|
|
893
|
+
from channels.layers import get_channel_layer
|
|
894
|
+
from asgiref.sync import async_to_sync
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
@receiver(post_save, sender=Task)
|
|
898
|
+
def broadcast_task_update(sender, instance, **kwargs):
|
|
899
|
+
channel_layer = get_channel_layer()
|
|
900
|
+
async_to_sync(channel_layer.group_send)(
|
|
901
|
+
f'dashboard_{instance.project_id}',
|
|
902
|
+
{
|
|
903
|
+
'type': 'dashboard_update',
|
|
904
|
+
'data': {
|
|
905
|
+
'task_id': str(instance.id),
|
|
906
|
+
'status': instance.status,
|
|
907
|
+
'updated_at': instance.updated_at.isoformat()
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
)
|
|
41
911
|
```
|
|
42
912
|
|
|
43
913
|
## Best Practices
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
- Use
|
|
914
|
+
|
|
915
|
+
### Do's
|
|
916
|
+
|
|
917
|
+
- Use UUID primary keys for public-facing IDs
|
|
918
|
+
- Use `select_related` and `prefetch_related` for query optimization
|
|
919
|
+
- Use custom managers for reusable query logic
|
|
920
|
+
- Use signals sparingly and for cross-cutting concerns only
|
|
921
|
+
- Use Django REST Framework serializers for validation
|
|
922
|
+
- Use soft deletes for important data
|
|
923
|
+
- Use database indexes for frequently queried fields
|
|
924
|
+
- Write comprehensive tests with fixtures
|
|
925
|
+
- Use environment variables for configuration
|
|
926
|
+
- Use Celery for background tasks
|
|
927
|
+
|
|
928
|
+
### Don'ts
|
|
929
|
+
|
|
930
|
+
- Don't use `ForeignKey` without `on_delete` consideration
|
|
931
|
+
- Don't query in loops (N+1 problem)
|
|
932
|
+
- Don't store sensitive data in plain text
|
|
933
|
+
- Don't use `Model.objects.all()` in production views
|
|
934
|
+
- Don't skip migrations in deployment
|
|
935
|
+
- Don't use raw SQL without parameterization
|
|
936
|
+
- Don't ignore database connection pooling
|
|
937
|
+
- Don't put business logic in views
|
|
938
|
+
- Don't use synchronous operations for I/O-heavy tasks
|
|
939
|
+
- Don't skip input validation
|
|
940
|
+
|
|
941
|
+
## References
|
|
942
|
+
|
|
943
|
+
- [Django Documentation](https://docs.djangoproject.com/)
|
|
944
|
+
- [Django REST Framework](https://www.django-rest-framework.org/)
|
|
945
|
+
- [Django Channels](https://channels.readthedocs.io/)
|
|
946
|
+
- [Celery Documentation](https://docs.celeryq.dev/)
|
|
947
|
+
- [Django Best Practices](https://django-best-practices.readthedocs.io/)
|