@dv.nghiem/flowdeck 0.2.4 → 0.3.1
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/README.md +24 -41
- package/dist/hooks/approval-hook.d.ts +6 -0
- package/dist/hooks/approval-hook.d.ts.map +1 -1
- package/dist/hooks/guard-rails.d.ts +0 -8
- package/dist/hooks/guard-rails.d.ts.map +1 -1
- package/dist/hooks/memory-hook.d.ts +21 -0
- package/dist/hooks/memory-hook.d.ts.map +1 -0
- package/dist/hooks/orchestrator-guard-hook.d.ts.map +1 -1
- package/dist/hooks/patch-trust.d.ts.map +1 -1
- package/dist/hooks/todo-hook.d.ts +1 -7
- package/dist/hooks/todo-hook.d.ts.map +1 -1
- package/dist/hooks/tool-guard.d.ts +1 -0
- package/dist/hooks/tool-guard.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +728 -428
- package/dist/services/memory-store.d.ts +40 -0
- package/dist/services/memory-store.d.ts.map +1 -0
- package/dist/services/policy-compiler.d.ts.map +1 -1
- package/dist/tools/memory-search.d.ts +3 -0
- package/dist/tools/memory-search.d.ts.map +1 -0
- package/docs/commands/fd-doctor.md +21 -0
- package/docs/commands/fd-quick.md +33 -0
- package/docs/commands/fd-reflect.md +23 -0
- package/docs/commands/fd-status.md +31 -0
- package/docs/commands/fd-translate-intent.md +17 -0
- package/docs/commands.md +209 -271
- package/docs/configuration.md +5 -2
- package/docs/index.md +22 -28
- package/docs/memory.md +69 -0
- package/docs/quick-start.md +1 -1
- package/package.json +1 -1
- package/src/commands/fd-deploy-check.md +131 -11
- package/src/commands/fd-new-project.md +14 -1
- package/src/commands/fd-quick.md +60 -0
- package/src/commands/fd-reflect.md +41 -2
- package/src/commands/fd-status.md +84 -0
- package/src/rules/README.md +8 -7
- package/src/skills/agent-harness-construction/SKILL.md +227 -0
- package/src/skills/api-design/SKILL.md +5 -0
- package/src/skills/backend-patterns/SKILL.md +105 -0
- package/src/skills/clean-architecture/SKILL.md +85 -0
- package/src/skills/cqrs/SKILL.md +230 -0
- package/src/skills/ddd-architecture/SKILL.md +104 -0
- package/src/skills/django-patterns/SKILL.md +304 -0
- package/src/skills/django-tdd/SKILL.md +297 -0
- package/src/skills/event-driven-architecture/SKILL.md +152 -0
- package/src/skills/frontend-pattern/SKILL.md +159 -0
- package/src/skills/hexagonal-architecture/SKILL.md +80 -0
- package/src/skills/layered-architecture/SKILL.md +64 -0
- package/src/skills/postgres-patterns/SKILL.md +74 -0
- package/src/skills/python-patterns/SKILL.md +5 -0
- package/src/skills/saga-architecture/SKILL.md +113 -0
- package/dist/tools/run-parallel.d.ts +0 -4
- package/dist/tools/run-parallel.d.ts.map +0 -1
- package/docs/command-migration.md +0 -175
- package/docs/commands/fd-analyze-change.md +0 -107
- package/docs/commands/fd-dashboard.md +0 -11
- package/docs/commands/fd-evaluate-risk.md +0 -134
- package/docs/commands/fd-guarded-edit.md +0 -105
- package/docs/commands/fd-progress.md +0 -11
- package/docs/commands/fd-review-code.md +0 -29
- package/docs/commands/fd-roadmap.md +0 -10
- package/docs/commands/fd-settings.md +0 -10
- package/docs/parallel-execution.md +0 -255
- package/src/commands/fd-analyze-change.md +0 -57
- package/src/commands/fd-approve.md +0 -64
- package/src/commands/fd-blast-radius.md +0 -49
- package/src/commands/fd-dashboard.md +0 -57
- package/src/commands/fd-evaluate-risk.md +0 -62
- package/src/commands/fd-guarded-edit.md +0 -69
- package/src/commands/fd-impact-radar.md +0 -51
- package/src/commands/fd-learn.md +0 -36
- package/src/commands/fd-progress.md +0 -50
- package/src/commands/fd-regression-predict.md +0 -57
- package/src/commands/fd-review-code.md +0 -96
- package/src/commands/fd-review-route.md +0 -54
- package/src/commands/fd-roadmap.md +0 -46
- package/src/commands/fd-settings.md +0 -57
- package/src/commands/fd-test-gap.md +0 -54
- package/src/commands/fd-volatility-map.md +0 -64
- package/src/commands/fd-workspace-status.md +0 -34
- package/src/skills/parallel-execute/SKILL.md +0 -92
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# ddd-architecture
|
|
2
|
+
|
|
3
|
+
## When to Activate
|
|
4
|
+
When modeling complex business domains where deep understanding of the problem space, ubiquitous language, and bounded contexts are critical for long-term maintainability.
|
|
5
|
+
|
|
6
|
+
## Steps
|
|
7
|
+
1. **Establish the bounded context** - Identify the explicit boundary within which a single model (ubiquitous language) holds.
|
|
8
|
+
2. **Build the domain model** - Create entities, value objects, aggregates, and domain events that reflect real business concepts.
|
|
9
|
+
3. **Define aggregates** - Group related entities and value objects under a root aggregate that enforces invariants.
|
|
10
|
+
4. **Identify domain events** - Capture meaningful business occurrences that other parts of the system may need to react to.
|
|
11
|
+
5. **Create domain services** - Model operations that don't naturally belong to a single entity or value object.
|
|
12
|
+
6. **Define repository interfaces** - Create ports for persisting and retrieving aggregates (implementation is infrastructure).
|
|
13
|
+
7. **Implement application services** - Orchestrate the domain model, handle transactions, and coordinate multiple aggregates.
|
|
14
|
+
8. **Establish anti-corruption layers** - Translate between external systems (legacy, third-party) and your domain model.
|
|
15
|
+
|
|
16
|
+
## Examples
|
|
17
|
+
```typescript
|
|
18
|
+
// Value Object - Immutable concept with equality
|
|
19
|
+
class Money {
|
|
20
|
+
constructor(
|
|
21
|
+
public readonly amount: number,
|
|
22
|
+
public readonly currency: Currency
|
|
23
|
+
) {}
|
|
24
|
+
|
|
25
|
+
static of(amount: number, currency: Currency): Money {
|
|
26
|
+
return new Money(Math.round(amount * 100) / 100, currency)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
add(other: Money): Money {
|
|
30
|
+
if (this.currency !== other.currency) {
|
|
31
|
+
throw new Error('Currency mismatch')
|
|
32
|
+
}
|
|
33
|
+
return Money.of(this.amount + other.amount, this.currency)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Aggregate Root - Enforces invariants for the aggregate
|
|
38
|
+
class Order extends AggregateRoot {
|
|
39
|
+
constructor(
|
|
40
|
+
private readonly id: OrderId,
|
|
41
|
+
private readonly customer: Customer,
|
|
42
|
+
private items: OrderItem[],
|
|
43
|
+
private status: OrderStatus
|
|
44
|
+
) {
|
|
45
|
+
super()
|
|
46
|
+
this.validate()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private validate(): void {
|
|
50
|
+
if (this.items.length === 0) {
|
|
51
|
+
throw new DomainException('Order must have at least one item')
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get total(): Money {
|
|
56
|
+
return this.items.reduce(
|
|
57
|
+
(sum, item) => sum.add(item.subtotal),
|
|
58
|
+
Money.of(0, Currency.USD)
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Business methods that enforce invariants
|
|
63
|
+
addItem(item: OrderItem): void {
|
|
64
|
+
if (this.status !== OrderStatus.DRAFT) {
|
|
65
|
+
throw new DomainException('Cannot add items to a non-draft order')
|
|
66
|
+
}
|
|
67
|
+
this.items.push(item)
|
|
68
|
+
this.addDomainEvent(new OrderItemAddedEvent(this.id, item))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
submit(): void {
|
|
72
|
+
if (!this.canSubmit()) {
|
|
73
|
+
throw new DomainException('Order cannot be submitted')
|
|
74
|
+
}
|
|
75
|
+
this.status = OrderStatus.SUBMITTED
|
|
76
|
+
this.addDomainEvent(new OrderSubmittedEvent(this))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private canSubmit(): boolean {
|
|
80
|
+
return this.status === OrderStatus.DRAFT && this.items.length > 0
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Domain Event - Business facts that may trigger reactions
|
|
85
|
+
class OrderSubmittedEvent extends DomainEvent {
|
|
86
|
+
constructor(public readonly order: Order) {
|
|
87
|
+
super('order.submitted', order.id)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Repository Interface (Port) - Persistence abstraction
|
|
92
|
+
interface OrderRepository {
|
|
93
|
+
findById(id: OrderId): Promise<Order | null>
|
|
94
|
+
findByCustomer(customerId: CustomerId): Promise<Order[]>
|
|
95
|
+
save(order: Order): Promise<void>
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Related Skills
|
|
100
|
+
- clean-architecture
|
|
101
|
+
- hexagonal-architecture
|
|
102
|
+
- layered-architecture
|
|
103
|
+
- saga-architecture
|
|
104
|
+
- backend-patterns
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: django-patterns
|
|
3
|
+
description: Django patterns covering models, ORM queries, views, class-based views, middleware, URL routing, forms, and project structure. Activate when writing or reviewing Django code.
|
|
4
|
+
origin: FlowDeck
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Django Patterns Skill
|
|
8
|
+
|
|
9
|
+
Idiomatic Django for production systems. Covers models, views, ORM patterns, and project layout.
|
|
10
|
+
|
|
11
|
+
## When to Activate
|
|
12
|
+
|
|
13
|
+
Activate when:
|
|
14
|
+
- Writing new Django apps or services
|
|
15
|
+
- Reviewing Django code for correctness and idiom
|
|
16
|
+
- Designing model relationships and ORM queries
|
|
17
|
+
- Building views with class-based views or function-based views
|
|
18
|
+
- Configuring URL routing and middleware
|
|
19
|
+
|
|
20
|
+
## Project Structure
|
|
21
|
+
|
|
22
|
+
### Standard Layout
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
manage.py
|
|
26
|
+
mysite/
|
|
27
|
+
__init__.py
|
|
28
|
+
settings.py
|
|
29
|
+
urls.py
|
|
30
|
+
wsgi.py
|
|
31
|
+
myapp/
|
|
32
|
+
__init__.py
|
|
33
|
+
models.py
|
|
34
|
+
views.py
|
|
35
|
+
urls.py
|
|
36
|
+
admin.py
|
|
37
|
+
apps.py
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Decoupled Layout (Recommended)
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
manage.py
|
|
44
|
+
myapp/
|
|
45
|
+
__init__.py
|
|
46
|
+
models.py
|
|
47
|
+
views.py
|
|
48
|
+
urls.py
|
|
49
|
+
mysite/
|
|
50
|
+
__init__.py
|
|
51
|
+
settings.py
|
|
52
|
+
urls.py
|
|
53
|
+
wsgi.py
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Import apps as top-level modules without project prefix.
|
|
57
|
+
|
|
58
|
+
## Models and ORM
|
|
59
|
+
|
|
60
|
+
### Basic Model Definition
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from django.db import models
|
|
64
|
+
|
|
65
|
+
class Reporter(models.Model):
|
|
66
|
+
full_name = models.CharField(max_length=70)
|
|
67
|
+
|
|
68
|
+
def __str__(self):
|
|
69
|
+
return self.full_name
|
|
70
|
+
|
|
71
|
+
class Article(models.Model):
|
|
72
|
+
pub_date = models.DateField()
|
|
73
|
+
headline = models.CharField(max_length=200)
|
|
74
|
+
content = models.TextField()
|
|
75
|
+
reporter = models.ForeignKey(Reporter, on_delete=models.CASCADE)
|
|
76
|
+
|
|
77
|
+
def __str__(self):
|
|
78
|
+
return self.headline
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Field Types and Options
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
class Book(models.Model):
|
|
85
|
+
class Status(models.TextChoices):
|
|
86
|
+
DRAFT = 'draft', 'Draft'
|
|
87
|
+
PUBLISHED = 'published', 'Published'
|
|
88
|
+
ARCHIVED = 'archived', 'Archived'
|
|
89
|
+
|
|
90
|
+
title = models.CharField(max_length=200)
|
|
91
|
+
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name='books')
|
|
92
|
+
isbn = models.CharField(max_length=13, unique=True)
|
|
93
|
+
published_date = models.DateField()
|
|
94
|
+
price = models.DecimalField(max_digits=10, decimal_places=2)
|
|
95
|
+
status = models.CharField(max_length=10, choices=Status.choices, default=Status.DRAFT)
|
|
96
|
+
tags = models.ManyToManyField('Tag', blank=True)
|
|
97
|
+
|
|
98
|
+
class Meta:
|
|
99
|
+
ordering = ['title']
|
|
100
|
+
indexes = [
|
|
101
|
+
models.Index(fields=['title', 'author']),
|
|
102
|
+
models.Index(fields=['published_date']),
|
|
103
|
+
]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### QuerySet Operations
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
# Create
|
|
110
|
+
author = Author.objects.create(name="Jane Doe", email="jane@example.com")
|
|
111
|
+
book = Book.objects.create(title="Django Mastery", author=author, isbn="9781234567890")
|
|
112
|
+
|
|
113
|
+
# Read with filtering
|
|
114
|
+
published_books = Book.objects.filter(status=Book.Status.PUBLISHED)
|
|
115
|
+
expensive_books = Book.objects.filter(price__gte=30.00)
|
|
116
|
+
|
|
117
|
+
# Complex queries with Q objects
|
|
118
|
+
from django.db.models import Q, F, Count, Avg
|
|
119
|
+
books = Book.objects.filter(
|
|
120
|
+
Q(title__icontains='django') | Q(author__name__icontains='django')
|
|
121
|
+
).exclude(status=Book.Status.ARCHIVED)
|
|
122
|
+
|
|
123
|
+
# Aggregations
|
|
124
|
+
author_stats = Author.objects.annotate(
|
|
125
|
+
book_count=Count('books'),
|
|
126
|
+
avg_price=Avg('books__price')
|
|
127
|
+
).filter(book_count__gt=0)
|
|
128
|
+
|
|
129
|
+
# Update with F expressions
|
|
130
|
+
Book.objects.filter(pk=1).update(price=F('price') * 1.1)
|
|
131
|
+
|
|
132
|
+
# Select related (prevents N+1)
|
|
133
|
+
books_with_authors = Book.objects.select_related('author').all()
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Views
|
|
137
|
+
|
|
138
|
+
### Class-Based Views
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from django.http import HttpResponse
|
|
142
|
+
from django.views import View
|
|
143
|
+
from django.views.generic import ListView, DetailView, CreateView
|
|
144
|
+
|
|
145
|
+
class MyView(View):
|
|
146
|
+
def get(self, request, *args, **kwargs):
|
|
147
|
+
return HttpResponse("Hello, World!")
|
|
148
|
+
|
|
149
|
+
# URL routing
|
|
150
|
+
from django.urls import path
|
|
151
|
+
urlpatterns = [
|
|
152
|
+
path("about/", MyView.as_view()),
|
|
153
|
+
]
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Generic Class-Based Views
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
class ArticleListView(ListView):
|
|
160
|
+
model = Article
|
|
161
|
+
template_name = "articles/list.html"
|
|
162
|
+
context_object_name = "articles"
|
|
163
|
+
|
|
164
|
+
def get_queryset(self):
|
|
165
|
+
return Article.objects.filter(status='published').select_related('author')
|
|
166
|
+
|
|
167
|
+
class ArticleDetailView(DetailView):
|
|
168
|
+
model = Article
|
|
169
|
+
template_name = "articles/detail.html"
|
|
170
|
+
context_object_name = "article"
|
|
171
|
+
|
|
172
|
+
class ArticleCreateView(CreateView):
|
|
173
|
+
model = Article
|
|
174
|
+
fields = ['title', 'content', 'author', 'status']
|
|
175
|
+
template_name = "articles/form.html"
|
|
176
|
+
success_url = reverse_lazy('article-list')
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Function-Based Views
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
from django.http import JsonResponse
|
|
183
|
+
from django.shortcuts import get_object_or_404
|
|
184
|
+
|
|
185
|
+
def article_detail(request, pk):
|
|
186
|
+
article = get_object_or_404(Article, pk=pk)
|
|
187
|
+
return JsonResponse({
|
|
188
|
+
'id': article.id,
|
|
189
|
+
'title': article.title,
|
|
190
|
+
'content': article.content,
|
|
191
|
+
})
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## URL Routing
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
from django.urls import path, include
|
|
198
|
+
|
|
199
|
+
urlpatterns = [
|
|
200
|
+
path("articles/", include("articles.urls")),
|
|
201
|
+
path("about/", AboutView.as_view(), name="about"),
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
# In articles/urls.py
|
|
205
|
+
from django.urls import path
|
|
206
|
+
from .views import ArticleListView, ArticleDetailView
|
|
207
|
+
|
|
208
|
+
urlpatterns = [
|
|
209
|
+
path("", ArticleListView.as_view(), name="article-list"),
|
|
210
|
+
path("<int:pk>/", ArticleDetailView.as_view(), name="article-detail"),
|
|
211
|
+
]
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Middleware
|
|
215
|
+
|
|
216
|
+
### Function-Based Middleware
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
def simple_middleware(get_response):
|
|
220
|
+
def middleware(request):
|
|
221
|
+
# Code executed before view
|
|
222
|
+
response = get_response(request)
|
|
223
|
+
# Code executed after view
|
|
224
|
+
return response
|
|
225
|
+
return middleware
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Adding to Settings
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
MIDDLEWARE = [
|
|
232
|
+
'django.middleware.security.SecurityMiddleware',
|
|
233
|
+
'django.contrib.sessions.middleware.SessionMiddleware',
|
|
234
|
+
'django.middleware.common.CommonMiddleware',
|
|
235
|
+
'myapp.middleware.simple_middleware',
|
|
236
|
+
]
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Forms
|
|
240
|
+
|
|
241
|
+
### Model Forms
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
from django import forms
|
|
245
|
+
from .models import Article
|
|
246
|
+
|
|
247
|
+
class ArticleForm(forms.ModelForm):
|
|
248
|
+
class Meta:
|
|
249
|
+
model = Article
|
|
250
|
+
fields = ['title', 'content', 'author', 'status']
|
|
251
|
+
|
|
252
|
+
def clean_title(self):
|
|
253
|
+
title = self.cleaned_data['title']
|
|
254
|
+
if 'spam' in title.lower():
|
|
255
|
+
raise forms.ValidationError("No spam allowed")
|
|
256
|
+
return title
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Common Pitfalls
|
|
260
|
+
|
|
261
|
+
### N+1 Query Problem
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
# Bad: causes N+1 queries
|
|
265
|
+
articles = Article.objects.all()
|
|
266
|
+
for article in articles:
|
|
267
|
+
print(article.author.name) # N additional queries
|
|
268
|
+
|
|
269
|
+
# Good: use select_related or prefetch_related
|
|
270
|
+
articles = Article.objects.select_related('author').all()
|
|
271
|
+
for article in articles:
|
|
272
|
+
print(article.author.name) # No additional queries
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Using Q Objects for Complex Queries
|
|
276
|
+
|
|
277
|
+
```python
|
|
278
|
+
from django.db.models import Q
|
|
279
|
+
|
|
280
|
+
# OR conditions
|
|
281
|
+
Book.objects.filter(Q(title__icontains='django') | Q(author__name__icontains='django'))
|
|
282
|
+
|
|
283
|
+
# AND with exclusion
|
|
284
|
+
Book.objects.filter(status='published').exclude(Q(title__icontains='old'))
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Bulk Operations
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
# Create multiple objects efficiently
|
|
291
|
+
Book.objects.bulk_create([
|
|
292
|
+
Book(title='Book 1', author=author),
|
|
293
|
+
Book(title='Book 2', author=author),
|
|
294
|
+
])
|
|
295
|
+
|
|
296
|
+
# Update multiple objects
|
|
297
|
+
Book.objects.filter(status='archived').update(status='published')
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Related Skills
|
|
301
|
+
|
|
302
|
+
- django-tdd
|
|
303
|
+
- python-patterns
|
|
304
|
+
- api-design
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: django-tdd
|
|
3
|
+
description: Test-driven development patterns for Django covering pytest, Django TestCase, factory_boy, test client, and best practices for testing Django applications.
|
|
4
|
+
origin: FlowDeck
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Django TDD Skill
|
|
8
|
+
|
|
9
|
+
Test-driven development workflow for Django applications. Covers pytest fixtures, Django TestCase, factory_boy, and testing patterns.
|
|
10
|
+
|
|
11
|
+
## When to Activate
|
|
12
|
+
|
|
13
|
+
Activate when:
|
|
14
|
+
- Writing new Django features using TDD workflow
|
|
15
|
+
- Adding tests to existing Django applications
|
|
16
|
+
- Debugging test failures in Django projects
|
|
17
|
+
- Setting up testing infrastructure for Django apps
|
|
18
|
+
|
|
19
|
+
## TDD Workflow
|
|
20
|
+
|
|
21
|
+
1. Write a failing test (RED)
|
|
22
|
+
2. Run the test - it should fail
|
|
23
|
+
3. Write minimal implementation (GREEN)
|
|
24
|
+
4. Run tests - they should pass
|
|
25
|
+
5. Refactor (IMPROVE)
|
|
26
|
+
6. Verify coverage
|
|
27
|
+
|
|
28
|
+
## Test Setup
|
|
29
|
+
|
|
30
|
+
### Using pytest with Django
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
# conftest.py
|
|
34
|
+
import pytest
|
|
35
|
+
import django
|
|
36
|
+
from django.conf import settings
|
|
37
|
+
|
|
38
|
+
@pytest.fixture(scope="session")
|
|
39
|
+
def django_db_setup():
|
|
40
|
+
settings.DATABASES["default"] = {
|
|
41
|
+
"ENGINE": "django.db.backends.sqlite3",
|
|
42
|
+
"NAME": ":memory:",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@pytest.fixture
|
|
46
|
+
def api_client():
|
|
47
|
+
from rest_framework.test import APIClient
|
|
48
|
+
return APIClient()
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Django TestCase
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from django.test import TestCase, Client
|
|
55
|
+
from django.urls import reverse
|
|
56
|
+
from django.contrib.auth import get_user_model
|
|
57
|
+
from .models import Article, Author
|
|
58
|
+
|
|
59
|
+
User = get_user_model()
|
|
60
|
+
|
|
61
|
+
class ArticleViewTest(TestCase):
|
|
62
|
+
def setUp(self):
|
|
63
|
+
self.client = Client()
|
|
64
|
+
self.user = User.objects.create_user(
|
|
65
|
+
email='test@example.com',
|
|
66
|
+
password='testpass123'
|
|
67
|
+
)
|
|
68
|
+
self.author = Author.objects.create(name='Author', email='a@test.com')
|
|
69
|
+
self.article = Article.objects.create(
|
|
70
|
+
title='Test',
|
|
71
|
+
content='Content',
|
|
72
|
+
author=self.author,
|
|
73
|
+
status='published'
|
|
74
|
+
)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Testing Views
|
|
78
|
+
|
|
79
|
+
### List View Tests
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
def test_article_list_view(self):
|
|
83
|
+
response = self.client.get(reverse('article-list'))
|
|
84
|
+
self.assertEqual(response.status_code, 200)
|
|
85
|
+
self.assertContains(response, 'Test')
|
|
86
|
+
self.assertTemplateUsed(response, 'articles/list.html')
|
|
87
|
+
self.assertEqual(len(response.context['articles']), 1)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Detail View Tests
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
def test_article_detail_view(self):
|
|
94
|
+
response = self.client.get(
|
|
95
|
+
reverse('article-detail', kwargs={'pk': self.article.pk})
|
|
96
|
+
)
|
|
97
|
+
self.assertEqual(response.status_code, 200)
|
|
98
|
+
self.assertContains(response, self.article.title)
|
|
99
|
+
|
|
100
|
+
def test_article_detail_not_found(self):
|
|
101
|
+
response = self.client.get(
|
|
102
|
+
reverse('article-detail', kwargs={'pk': 9999})
|
|
103
|
+
)
|
|
104
|
+
self.assertEqual(response.status_code, 404)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Authentication Tests
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
def test_create_article_requires_login(self):
|
|
111
|
+
response = self.client.get(reverse('article-create'))
|
|
112
|
+
self.assertRedirects(response, '/accounts/login/?next=/articles/create/')
|
|
113
|
+
|
|
114
|
+
def test_create_article_authenticated(self):
|
|
115
|
+
self.client.login(email='test@example.com', password='testpass123')
|
|
116
|
+
response = self.client.post(reverse('article-create'), {
|
|
117
|
+
'title': 'New Article',
|
|
118
|
+
'content': 'New content',
|
|
119
|
+
'status': 'draft',
|
|
120
|
+
})
|
|
121
|
+
self.assertEqual(response.status_code, 302)
|
|
122
|
+
self.assertTrue(Article.objects.filter(title='New Article').exists())
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### JSON API Tests
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
def test_json_response(self):
|
|
129
|
+
response = self.client.get(
|
|
130
|
+
reverse('api-articles'),
|
|
131
|
+
content_type='application/json'
|
|
132
|
+
)
|
|
133
|
+
self.assertEqual(response.status_code, 200)
|
|
134
|
+
data = response.json()
|
|
135
|
+
self.assertIn('articles', data)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Testing Models
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
def test_article_creation(self):
|
|
142
|
+
article = Article.objects.create(
|
|
143
|
+
title='Test Article',
|
|
144
|
+
content='Test content',
|
|
145
|
+
author=self.author,
|
|
146
|
+
status='draft'
|
|
147
|
+
)
|
|
148
|
+
self.assertEqual(article.title, 'Test Article')
|
|
149
|
+
self.assertEqual(article.status, 'draft')
|
|
150
|
+
self.assertEqual(str(article), 'Test Article')
|
|
151
|
+
|
|
152
|
+
def test_article_ordering(self):
|
|
153
|
+
Article.objects.create(title='Second', author=self.author)
|
|
154
|
+
Article.objects.create(title='First', author=self.author)
|
|
155
|
+
articles = list(Article.objects.all())
|
|
156
|
+
self.assertEqual(articles[0].title, 'First')
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Factory Boy Fixtures
|
|
160
|
+
|
|
161
|
+
### Defining Factories
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
# factories.py
|
|
165
|
+
import factory
|
|
166
|
+
from factory.django import DjangoModelFactory
|
|
167
|
+
from .models import Author, Article
|
|
168
|
+
|
|
169
|
+
class AuthorFactory(DjangoModelFactory):
|
|
170
|
+
class Meta:
|
|
171
|
+
model = Author
|
|
172
|
+
|
|
173
|
+
name = factory.Sequence(lambda n: f"Author {n}")
|
|
174
|
+
email = factory.LazyAttribute(lambda obj: f"{obj.name.replace(' ', '')}@example.com")
|
|
175
|
+
|
|
176
|
+
class ArticleFactory(DjangoModelFactory):
|
|
177
|
+
class Meta:
|
|
178
|
+
model = Article
|
|
179
|
+
|
|
180
|
+
title = factory.Sequence(lambda n: f"Article {n}")
|
|
181
|
+
content = "Test content"
|
|
182
|
+
author = factory.SubFactory(AuthorFactory)
|
|
183
|
+
status = 'draft'
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Using Factories in Tests
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from .factories import AuthorFactory, ArticleFactory
|
|
190
|
+
|
|
191
|
+
def test_article_with_factory(self):
|
|
192
|
+
author = AuthorFactory(name="Jane Doe")
|
|
193
|
+
article = ArticleFactory(title="Test", author=author)
|
|
194
|
+
assert article.author.name == "Jane Doe"
|
|
195
|
+
assert article.title == "Test"
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Pytest Fixtures
|
|
199
|
+
|
|
200
|
+
### Basic Fixture
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
import pytest
|
|
204
|
+
|
|
205
|
+
@pytest.fixture
|
|
206
|
+
def sample_data():
|
|
207
|
+
return {"name": "test", "value": 42}
|
|
208
|
+
|
|
209
|
+
def test_sample_data(sample_data):
|
|
210
|
+
assert sample_data["name"] == "test"
|
|
211
|
+
assert sample_data["value"] == 42
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Fixture with Teardown
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
import tempfile
|
|
218
|
+
import os
|
|
219
|
+
|
|
220
|
+
@pytest.fixture
|
|
221
|
+
def temp_file():
|
|
222
|
+
fd, path = tempfile.mkstemp()
|
|
223
|
+
os.write(fd, b"test content")
|
|
224
|
+
os.close(fd)
|
|
225
|
+
yield path
|
|
226
|
+
os.unlink(path)
|
|
227
|
+
|
|
228
|
+
def test_temp_file(temp_file):
|
|
229
|
+
assert os.path.exists(temp_file)
|
|
230
|
+
with open(temp_file) as f:
|
|
231
|
+
assert f.read() == "test content"
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Parametrized Fixtures
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
@pytest.fixture(params=["mysql", "postgresql", "sqlite"])
|
|
238
|
+
def database_type(request):
|
|
239
|
+
return request.param
|
|
240
|
+
|
|
241
|
+
def test_database_type(database_type):
|
|
242
|
+
assert database_type in ["mysql", "postgresql", "sqlite"]
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Common Patterns
|
|
246
|
+
|
|
247
|
+
### Testing Forms
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
def test_form_valid(self):
|
|
251
|
+
form_data = {
|
|
252
|
+
'title': 'New Article',
|
|
253
|
+
'content': 'Content',
|
|
254
|
+
'author': self.author.pk,
|
|
255
|
+
'status': 'draft',
|
|
256
|
+
}
|
|
257
|
+
form = ArticleForm(data=form_data)
|
|
258
|
+
self.assertTrue(form.is_valid())
|
|
259
|
+
|
|
260
|
+
def test_form_invalid(self):
|
|
261
|
+
form_data = {'title': '', 'content': 'Content'}
|
|
262
|
+
form = ArticleForm(data=form_data)
|
|
263
|
+
self.assertFalse(form.is_valid())
|
|
264
|
+
self.assertIn('title', form.errors)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Testing Middleware
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
def test_middleware_process_request(self):
|
|
271
|
+
response = self.client.get('/articles/')
|
|
272
|
+
self.assertEqual(response.status_code, 200)
|
|
273
|
+
# Check middleware added expected headers or behavior
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Testing Signals
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
def test_signal_on_save(self):
|
|
280
|
+
article = ArticleFactory()
|
|
281
|
+
# Verify signal handlers executed (e.g., notifications sent)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Coverage Verification
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
# Run with coverage
|
|
288
|
+
pytest --cov=myapp --cov-report=html
|
|
289
|
+
|
|
290
|
+
# Minimum 80% coverage required
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Related Skills
|
|
294
|
+
|
|
295
|
+
- django-patterns
|
|
296
|
+
- python-patterns
|
|
297
|
+
- tdd-workflow
|