@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.
Files changed (82) hide show
  1. package/README.md +24 -41
  2. package/dist/hooks/approval-hook.d.ts +6 -0
  3. package/dist/hooks/approval-hook.d.ts.map +1 -1
  4. package/dist/hooks/guard-rails.d.ts +0 -8
  5. package/dist/hooks/guard-rails.d.ts.map +1 -1
  6. package/dist/hooks/memory-hook.d.ts +21 -0
  7. package/dist/hooks/memory-hook.d.ts.map +1 -0
  8. package/dist/hooks/orchestrator-guard-hook.d.ts.map +1 -1
  9. package/dist/hooks/patch-trust.d.ts.map +1 -1
  10. package/dist/hooks/todo-hook.d.ts +1 -7
  11. package/dist/hooks/todo-hook.d.ts.map +1 -1
  12. package/dist/hooks/tool-guard.d.ts +1 -0
  13. package/dist/hooks/tool-guard.d.ts.map +1 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +728 -428
  16. package/dist/services/memory-store.d.ts +40 -0
  17. package/dist/services/memory-store.d.ts.map +1 -0
  18. package/dist/services/policy-compiler.d.ts.map +1 -1
  19. package/dist/tools/memory-search.d.ts +3 -0
  20. package/dist/tools/memory-search.d.ts.map +1 -0
  21. package/docs/commands/fd-doctor.md +21 -0
  22. package/docs/commands/fd-quick.md +33 -0
  23. package/docs/commands/fd-reflect.md +23 -0
  24. package/docs/commands/fd-status.md +31 -0
  25. package/docs/commands/fd-translate-intent.md +17 -0
  26. package/docs/commands.md +209 -271
  27. package/docs/configuration.md +5 -2
  28. package/docs/index.md +22 -28
  29. package/docs/memory.md +69 -0
  30. package/docs/quick-start.md +1 -1
  31. package/package.json +1 -1
  32. package/src/commands/fd-deploy-check.md +131 -11
  33. package/src/commands/fd-new-project.md +14 -1
  34. package/src/commands/fd-quick.md +60 -0
  35. package/src/commands/fd-reflect.md +41 -2
  36. package/src/commands/fd-status.md +84 -0
  37. package/src/rules/README.md +8 -7
  38. package/src/skills/agent-harness-construction/SKILL.md +227 -0
  39. package/src/skills/api-design/SKILL.md +5 -0
  40. package/src/skills/backend-patterns/SKILL.md +105 -0
  41. package/src/skills/clean-architecture/SKILL.md +85 -0
  42. package/src/skills/cqrs/SKILL.md +230 -0
  43. package/src/skills/ddd-architecture/SKILL.md +104 -0
  44. package/src/skills/django-patterns/SKILL.md +304 -0
  45. package/src/skills/django-tdd/SKILL.md +297 -0
  46. package/src/skills/event-driven-architecture/SKILL.md +152 -0
  47. package/src/skills/frontend-pattern/SKILL.md +159 -0
  48. package/src/skills/hexagonal-architecture/SKILL.md +80 -0
  49. package/src/skills/layered-architecture/SKILL.md +64 -0
  50. package/src/skills/postgres-patterns/SKILL.md +74 -0
  51. package/src/skills/python-patterns/SKILL.md +5 -0
  52. package/src/skills/saga-architecture/SKILL.md +113 -0
  53. package/dist/tools/run-parallel.d.ts +0 -4
  54. package/dist/tools/run-parallel.d.ts.map +0 -1
  55. package/docs/command-migration.md +0 -175
  56. package/docs/commands/fd-analyze-change.md +0 -107
  57. package/docs/commands/fd-dashboard.md +0 -11
  58. package/docs/commands/fd-evaluate-risk.md +0 -134
  59. package/docs/commands/fd-guarded-edit.md +0 -105
  60. package/docs/commands/fd-progress.md +0 -11
  61. package/docs/commands/fd-review-code.md +0 -29
  62. package/docs/commands/fd-roadmap.md +0 -10
  63. package/docs/commands/fd-settings.md +0 -10
  64. package/docs/parallel-execution.md +0 -255
  65. package/src/commands/fd-analyze-change.md +0 -57
  66. package/src/commands/fd-approve.md +0 -64
  67. package/src/commands/fd-blast-radius.md +0 -49
  68. package/src/commands/fd-dashboard.md +0 -57
  69. package/src/commands/fd-evaluate-risk.md +0 -62
  70. package/src/commands/fd-guarded-edit.md +0 -69
  71. package/src/commands/fd-impact-radar.md +0 -51
  72. package/src/commands/fd-learn.md +0 -36
  73. package/src/commands/fd-progress.md +0 -50
  74. package/src/commands/fd-regression-predict.md +0 -57
  75. package/src/commands/fd-review-code.md +0 -96
  76. package/src/commands/fd-review-route.md +0 -54
  77. package/src/commands/fd-roadmap.md +0 -46
  78. package/src/commands/fd-settings.md +0 -57
  79. package/src/commands/fd-test-gap.md +0 -54
  80. package/src/commands/fd-volatility-map.md +0 -64
  81. package/src/commands/fd-workspace-status.md +0 -34
  82. 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