omgkit 2.1.0 → 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.
Files changed (56) hide show
  1. package/package.json +1 -1
  2. package/plugin/skills/SKILL_STANDARDS.md +743 -0
  3. package/plugin/skills/databases/mongodb/SKILL.md +797 -28
  4. package/plugin/skills/databases/postgresql/SKILL.md +494 -18
  5. package/plugin/skills/databases/prisma/SKILL.md +776 -30
  6. package/plugin/skills/databases/redis/SKILL.md +885 -25
  7. package/plugin/skills/devops/aws/SKILL.md +686 -28
  8. package/plugin/skills/devops/docker/SKILL.md +466 -18
  9. package/plugin/skills/devops/github-actions/SKILL.md +684 -29
  10. package/plugin/skills/devops/kubernetes/SKILL.md +621 -24
  11. package/plugin/skills/frameworks/django/SKILL.md +920 -20
  12. package/plugin/skills/frameworks/express/SKILL.md +1361 -35
  13. package/plugin/skills/frameworks/fastapi/SKILL.md +1260 -33
  14. package/plugin/skills/frameworks/laravel/SKILL.md +1244 -31
  15. package/plugin/skills/frameworks/nestjs/SKILL.md +1005 -26
  16. package/plugin/skills/frameworks/nextjs/SKILL.md +407 -44
  17. package/plugin/skills/frameworks/rails/SKILL.md +594 -28
  18. package/plugin/skills/frameworks/react/SKILL.md +1006 -32
  19. package/plugin/skills/frameworks/spring/SKILL.md +528 -35
  20. package/plugin/skills/frameworks/vue/SKILL.md +1296 -27
  21. package/plugin/skills/frontend/accessibility/SKILL.md +1108 -34
  22. package/plugin/skills/frontend/frontend-design/SKILL.md +1304 -26
  23. package/plugin/skills/frontend/responsive/SKILL.md +847 -21
  24. package/plugin/skills/frontend/shadcn-ui/SKILL.md +976 -38
  25. package/plugin/skills/frontend/tailwindcss/SKILL.md +831 -35
  26. package/plugin/skills/frontend/threejs/SKILL.md +1298 -29
  27. package/plugin/skills/languages/javascript/SKILL.md +935 -31
  28. package/plugin/skills/languages/python/SKILL.md +489 -25
  29. package/plugin/skills/languages/typescript/SKILL.md +379 -30
  30. package/plugin/skills/methodology/brainstorming/SKILL.md +597 -23
  31. package/plugin/skills/methodology/defense-in-depth/SKILL.md +832 -34
  32. package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +665 -31
  33. package/plugin/skills/methodology/executing-plans/SKILL.md +556 -24
  34. package/plugin/skills/methodology/finishing-development-branch/SKILL.md +595 -25
  35. package/plugin/skills/methodology/problem-solving/SKILL.md +429 -61
  36. package/plugin/skills/methodology/receiving-code-review/SKILL.md +536 -24
  37. package/plugin/skills/methodology/requesting-code-review/SKILL.md +632 -21
  38. package/plugin/skills/methodology/root-cause-tracing/SKILL.md +641 -30
  39. package/plugin/skills/methodology/sequential-thinking/SKILL.md +262 -3
  40. package/plugin/skills/methodology/systematic-debugging/SKILL.md +571 -32
  41. package/plugin/skills/methodology/test-driven-development/SKILL.md +779 -24
  42. package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +691 -29
  43. package/plugin/skills/methodology/token-optimization/SKILL.md +598 -29
  44. package/plugin/skills/methodology/verification-before-completion/SKILL.md +543 -22
  45. package/plugin/skills/methodology/writing-plans/SKILL.md +590 -18
  46. package/plugin/skills/omega/omega-architecture/SKILL.md +838 -39
  47. package/plugin/skills/omega/omega-coding/SKILL.md +636 -39
  48. package/plugin/skills/omega/omega-sprint/SKILL.md +855 -48
  49. package/plugin/skills/omega/omega-testing/SKILL.md +940 -41
  50. package/plugin/skills/omega/omega-thinking/SKILL.md +703 -50
  51. package/plugin/skills/security/better-auth/SKILL.md +1065 -28
  52. package/plugin/skills/security/oauth/SKILL.md +968 -31
  53. package/plugin/skills/security/owasp/SKILL.md +894 -33
  54. package/plugin/skills/testing/playwright/SKILL.md +764 -38
  55. package/plugin/skills/testing/pytest/SKILL.md +873 -36
  56. package/plugin/skills/testing/vitest/SKILL.md +980 -35
@@ -1,47 +1,947 @@
1
1
  ---
2
2
  name: django
3
- description: Django development. Use for Django projects, ORM, admin, templates.
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 Skill
16
+ # Django
7
17
 
8
- ## Patterns
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(models.Model):
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
- ### View
23
- ```python
24
- from django.views import View
25
- from django.http import JsonResponse
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
- class UserView(View):
28
- def get(self, request, id):
29
- user = User.objects.get(id=id)
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
- ### Serializer (DRF)
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 = ['id', 'email', 'created_at']
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
- - Use class-based views
45
- - Use Django REST Framework
46
- - Use migrations
47
- - Use signals sparingly
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/)