cc-codeconductor 0.2.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/LICENSE +18 -0
- package/README.md +319 -0
- package/dist/index.js +12741 -0
- package/package.json +51 -0
- package/policy.yml +124 -0
- package/presets/claude/CLAUDE.md +598 -0
- package/presets/claude/commands/cc/feature.md +115 -0
- package/presets/claude/commands/cc/fix.md +121 -0
- package/presets/claude/commands/cc/refactor.md +148 -0
- package/presets/claude/commands/cc/review.md +126 -0
- package/presets/claude/commands/cc/tdd-cycle.md +226 -0
- package/presets/claude/commands/cc/test-plan.md +138 -0
- package/presets/claude/settings.json +37 -0
- package/presets/claude/skills/api-versioning/SKILL.md +389 -0
- package/presets/claude/skills/django-orm/SKILL.md +455 -0
- package/presets/claude/skills/django-testing/SKILL.md +409 -0
- package/presets/claude/skills/jpa-postgres/SKILL.md +618 -0
- package/presets/claude/skills/python/SKILL.md +606 -0
- package/presets/claude/skills/python-django-stack/SKILL.md +492 -0
- package/presets/claude/skills/python-fastapi-stack/SKILL.md +457 -0
- package/presets/claude/skills/spring-boot-feature/SKILL.md +558 -0
- package/presets/claude/skills/spring-boot-kotlin/SKILL.md +400 -0
- package/presets/claude/skills/sqlalchemy/SKILL.md +466 -0
- package/presets/claude/skills/testing-strategy/SKILL.md +479 -0
- package/presets/codex/AGENTS.md +883 -0
- package/presets/codex/README.md +102 -0
- package/presets/codex/skills/api-versioning/SKILL.md +389 -0
- package/presets/codex/skills/django-orm/SKILL.md +455 -0
- package/presets/codex/skills/django-testing/SKILL.md +409 -0
- package/presets/codex/skills/jpa-postgres/SKILL.md +618 -0
- package/presets/codex/skills/python/SKILL.md +606 -0
- package/presets/codex/skills/python-django-stack/SKILL.md +492 -0
- package/presets/codex/skills/python-fastapi-stack/SKILL.md +457 -0
- package/presets/codex/skills/spring-boot-feature/SKILL.md +558 -0
- package/presets/codex/skills/spring-boot-kotlin/SKILL.md +400 -0
- package/presets/codex/skills/sqlalchemy/SKILL.md +466 -0
- package/presets/codex/skills/testing-strategy/SKILL.md +479 -0
- package/presets/opencode/README.md +191 -0
- package/presets/opencode/agents/architect.md +133 -0
- package/presets/opencode/agents/docs.md +113 -0
- package/presets/opencode/agents/implementer.md +116 -0
- package/presets/opencode/agents/orchestrator.md +248 -0
- package/presets/opencode/agents/repo-explorer.md +119 -0
- package/presets/opencode/agents/reviewer.md +130 -0
- package/presets/opencode/agents/task-coach.md +103 -0
- package/presets/opencode/agents/tester.md +269 -0
- package/presets/opencode/commands/cc-feature.md +114 -0
- package/presets/opencode/commands/cc-fix.md +123 -0
- package/presets/opencode/commands/cc-refactor.md +148 -0
- package/presets/opencode/commands/cc-review.md +141 -0
- package/presets/opencode/commands/cc-tdd-cycle.md +225 -0
- package/presets/opencode/commands/cc-test-plan.md +144 -0
- package/presets/opencode/opencode.jsonc +89 -0
- package/presets/opencode/prompts/v0.1.0/architect.md +213 -0
- package/presets/opencode/prompts/v0.1.0/docs.md +181 -0
- package/presets/opencode/prompts/v0.1.0/implementer.md +154 -0
- package/presets/opencode/prompts/v0.1.0/orchestrator.md +169 -0
- package/presets/opencode/prompts/v0.1.0/repo-explorer.md +102 -0
- package/presets/opencode/prompts/v0.1.0/reviewer.md +183 -0
- package/presets/opencode/prompts/v0.1.0/task-coach.md +142 -0
- package/presets/opencode/prompts/v0.1.0/tester.md +160 -0
- package/presets/opencode/prompts/v0.2.0/architect.md +219 -0
- package/presets/opencode/prompts/v0.2.0/docs.md +187 -0
- package/presets/opencode/prompts/v0.2.0/implementer.md +160 -0
- package/presets/opencode/prompts/v0.2.0/orchestrator.md +238 -0
- package/presets/opencode/prompts/v0.2.0/repo-explorer.md +108 -0
- package/presets/opencode/prompts/v0.2.0/reviewer.md +190 -0
- package/presets/opencode/prompts/v0.2.0/task-coach.md +153 -0
- package/presets/opencode/prompts/v0.2.0/tester.md +249 -0
- package/presets/opencode/skills/api-versioning/SKILL.md +388 -0
- package/presets/opencode/skills/astro/SKILL.md +313 -0
- package/presets/opencode/skills/code-review/SKILL.md +202 -0
- package/presets/opencode/skills/django-orm/SKILL.md +455 -0
- package/presets/opencode/skills/django-testing/SKILL.md +408 -0
- package/presets/opencode/skills/django-uv/SKILL.md +400 -0
- package/presets/opencode/skills/jpa-postgres/SKILL.md +617 -0
- package/presets/opencode/skills/nextjs-typescript/SKILL.md +385 -0
- package/presets/opencode/skills/python/SKILL.md +605 -0
- package/presets/opencode/skills/python-django-stack/SKILL.md +491 -0
- package/presets/opencode/skills/python-fastapi-stack/SKILL.md +456 -0
- package/presets/opencode/skills/security/SKILL.md +376 -0
- package/presets/opencode/skills/spring-boot-feature/SKILL.md +557 -0
- package/presets/opencode/skills/spring-boot-kotlin/SKILL.md +399 -0
- package/presets/opencode/skills/spring-boot-testing-strategy/SKILL.md +470 -0
- package/presets/opencode/skills/sqlalchemy/SKILL.md +465 -0
- package/presets/opencode/skills/testing-tdd/SKILL.md +586 -0
- package/src/presets/council/council.yml +59 -0
- package/src/presets/manifests/claude.yml +23 -0
- package/src/presets/manifests/codex.yml +12 -0
- package/src/presets/manifests/opencode.yml +18 -0
- package/src/presets/models/claude.yml +37 -0
- package/src/presets/models/codex.yml +38 -0
- package/src/presets/models/opencode.yml +37 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: python-django-stack
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
name: Python Django Stack
|
|
5
|
+
description: >
|
|
6
|
+
Python and Django conventions for multi-tenant SaaS POS projects:
|
|
7
|
+
views, services, naming, JSON APIs, PDFs, and cart patterns.
|
|
8
|
+
Trigger: When writing any view, service, API endpoint, or model code in apps/.
|
|
9
|
+
|
|
10
|
+
compatibility:
|
|
11
|
+
tools: [claude, codex, opencode]
|
|
12
|
+
stacks:
|
|
13
|
+
languages: [python]
|
|
14
|
+
frameworks: [django, django-tenants, django-rest-framework, reportlab]
|
|
15
|
+
|
|
16
|
+
risk:
|
|
17
|
+
level: medium
|
|
18
|
+
can_execute_shell: true
|
|
19
|
+
can_modify_files: true
|
|
20
|
+
requires_network: false
|
|
21
|
+
|
|
22
|
+
inputs:
|
|
23
|
+
- view files (views.py)
|
|
24
|
+
- service files (services.py)
|
|
25
|
+
- model files (models.py)
|
|
26
|
+
- URL configuration files (urls.py)
|
|
27
|
+
- existing app structure under apps/
|
|
28
|
+
|
|
29
|
+
outputs:
|
|
30
|
+
- FBV and CBV view implementations
|
|
31
|
+
- service layer classes with static methods
|
|
32
|
+
- JSON API responses with correct status codes
|
|
33
|
+
- pagination with offset/limit pattern
|
|
34
|
+
- PDF generation functions (reportlab)
|
|
35
|
+
- cart session implementations
|
|
36
|
+
- URL configurations with namespaces
|
|
37
|
+
- model definitions with indexes and choices
|
|
38
|
+
|
|
39
|
+
quality:
|
|
40
|
+
reviewed_by: codeconductor-core
|
|
41
|
+
version: 0.1.0
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## When to Use
|
|
45
|
+
|
|
46
|
+
- Writing any new view, service method, or API endpoint
|
|
47
|
+
- Designing a new feature and deciding where logic lives
|
|
48
|
+
- Adding a JSON API response
|
|
49
|
+
- Implementing a PDF report or cart operation
|
|
50
|
+
- Structuring the architecture of a new app
|
|
51
|
+
|
|
52
|
+
## Django Project Structure
|
|
53
|
+
|
|
54
|
+
### Recommended Apps
|
|
55
|
+
|
|
56
|
+
A well-structured Django project follows modular app patterns:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
apps/
|
|
60
|
+
├── core/ # Configuration, shared models (Store)
|
|
61
|
+
├── users/ # User model, authentication
|
|
62
|
+
├── employees/ # Employees (tenant)
|
|
63
|
+
├── catalog/ # Products, categories (tenant)
|
|
64
|
+
├── cart/ # Storefront cart (tenant)
|
|
65
|
+
├── orders/ # Orders (tenant)
|
|
66
|
+
├── pos/ # Point of Sale (tenant)
|
|
67
|
+
├── storefront/ # Public store (tenant)
|
|
68
|
+
└── analytics/ # Reports and metrics (tenant)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Per-App File Structure
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
apps/catalog/
|
|
75
|
+
├── __init__.py
|
|
76
|
+
├── models.py # Models (Product, Category, ProductImage)
|
|
77
|
+
├── views.py # Views
|
|
78
|
+
├── urls.py # Routes
|
|
79
|
+
├── services.py # Business logic
|
|
80
|
+
├── serializers.py # Serialization (if using DRF)
|
|
81
|
+
├── reports.py # PDF reports
|
|
82
|
+
├── signals.py # Signals
|
|
83
|
+
├── admin.py # Admin config
|
|
84
|
+
├── tests/
|
|
85
|
+
│ ├── __init__.py
|
|
86
|
+
│ └── test_{feature}.py
|
|
87
|
+
└── migrations/
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## View Patterns
|
|
91
|
+
|
|
92
|
+
### FBV vs CBV
|
|
93
|
+
|
|
94
|
+
| Type | When to Use | Examples |
|
|
95
|
+
| ------------------------ | ---------------------------------------------------------------------- | ----------------------------------------------------- |
|
|
96
|
+
| **FBV** (Function-Based) | All API endpoints, all POS/manager views, any view with business logic | `api_cart_add`, `api_search_products`, `pos_checkout` |
|
|
97
|
+
| **CBV** (Class-Based) | Only basic storefront list/detail | `HomeView`, `CategoryListView`, `ProductDetailView` |
|
|
98
|
+
|
|
99
|
+
**Rule**: CBV for generic read-only views with simple templates. FBV for
|
|
100
|
+
everything else (APIs, complex logic, POS).
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
# FBV — always for APIs
|
|
104
|
+
@require_pos_access
|
|
105
|
+
@require_POST
|
|
106
|
+
def api_cart_add(request):
|
|
107
|
+
cart = POSCart(request)
|
|
108
|
+
data = json.loads(request.body)
|
|
109
|
+
cart.add(data["product_id"], data["quantity"])
|
|
110
|
+
return JsonResponse({"success": True, "cart_count": cart.count()})
|
|
111
|
+
|
|
112
|
+
# CBV — only for basic storefront
|
|
113
|
+
class ProductDetailView(TemplateView):
|
|
114
|
+
template_name = "storefront/product.html"
|
|
115
|
+
|
|
116
|
+
def get_context_data(self, **kwargs):
|
|
117
|
+
ctx = super().get_context_data(**kwargs)
|
|
118
|
+
ctx["product"] = get_object_or_404(Product, slug=self.kwargs["slug"])
|
|
119
|
+
return ctx
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Decorator Order
|
|
123
|
+
|
|
124
|
+
Custom access decorator outermost, HTTP method decorator innermost:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
@require_pos_access # outermost — runs first, guards access
|
|
128
|
+
@require_POST # innermost — closest to the function
|
|
129
|
+
def api_cart_add(request):
|
|
130
|
+
...
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Always use** `@require_POST` / `@require_GET` explicitly. Never
|
|
134
|
+
`if request.method == "POST"` inside an API view.
|
|
135
|
+
|
|
136
|
+
## Service Layer
|
|
137
|
+
|
|
138
|
+
Business logic lives in `services.py` modules. Services are classes with
|
|
139
|
+
`@staticmethod` only — no instance state, no `__init__`:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
class OrderService:
|
|
143
|
+
@staticmethod
|
|
144
|
+
def create_order(order: Order, cart: Cart) -> Order:
|
|
145
|
+
with transaction.atomic():
|
|
146
|
+
order.save()
|
|
147
|
+
OrderItem.objects.bulk_create(items)
|
|
148
|
+
Product.objects.bulk_update(products, ["stock"])
|
|
149
|
+
cart.clear()
|
|
150
|
+
return order
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def cancel_order(order_id: int, reason: str) -> Order:
|
|
154
|
+
...
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Views call `OrderService.create_order(...)`. **No business logic in views.**
|
|
158
|
+
|
|
159
|
+
### When to Create a Service
|
|
160
|
+
|
|
161
|
+
- Complex business logic involving multiple models
|
|
162
|
+
- Transactions spanning multiple DB operations
|
|
163
|
+
- Logic reusable across multiple views
|
|
164
|
+
- Anything that isn't "delegate to another component"
|
|
165
|
+
|
|
166
|
+
## Naming Conventions
|
|
167
|
+
|
|
168
|
+
| Pattern | Convention | Examples |
|
|
169
|
+
| ---------------------- | -------------------------------------------- | --------------------------------------------------------------- |
|
|
170
|
+
| AJAX API views | `api_{resource}_{action}` | `api_cart_add`, `api_search_products`, `api_temporal_cart_save` |
|
|
171
|
+
| Module-private helpers | leading `_` | `_build_pos_order`, `_sort_by_category` |
|
|
172
|
+
| Module constants | `UPPER_SNAKE_CASE` | `VALID_PAYMENT_METHODS = {"cash", "transfer", "card"}` |
|
|
173
|
+
| URL names | `{scope}_{resource}_{action}` | `api_catalog_products`, `pos_checkout` |
|
|
174
|
+
| Service methods | `camelCase` (PEP8 inconsistent but accepted) | `createOrder`, `calculateTotal` |
|
|
175
|
+
| Model methods | `snake_case` | `get_display_price()`, `is_available()` |
|
|
176
|
+
|
|
177
|
+
## JSON API Response Shapes
|
|
178
|
+
|
|
179
|
+
### List Endpoint with Pagination
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
return JsonResponse({
|
|
183
|
+
"results": [...],
|
|
184
|
+
"total": N,
|
|
185
|
+
"has_more": bool,
|
|
186
|
+
})
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Successful Mutation
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
return JsonResponse({"success": True, "cart_count": N})
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Error Response
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
return JsonResponse({"success": False, "error": "error message"}, status=400)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### HTTP Status Codes
|
|
202
|
+
|
|
203
|
+
- `200` — OK, success
|
|
204
|
+
- `201` — Created
|
|
205
|
+
- `400` — Client error (invalid data, missing fields)
|
|
206
|
+
- `404` — Not found
|
|
207
|
+
- `500` — Server error
|
|
208
|
+
|
|
209
|
+
**Never return** `200` with `"success": false`. Use the correct status code.
|
|
210
|
+
|
|
211
|
+
## Pagination
|
|
212
|
+
|
|
213
|
+
### Offset/Limit — No Page Numbers
|
|
214
|
+
|
|
215
|
+
Always offset + limit, never page numbers. Default 20 items, max 100:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
offset = int(request.GET.get("offset", 0))
|
|
219
|
+
limit = min(int(request.GET.get("limit", 20)), 100)
|
|
220
|
+
|
|
221
|
+
total = qs.count()
|
|
222
|
+
page = qs[offset:offset + limit]
|
|
223
|
+
|
|
224
|
+
return JsonResponse({
|
|
225
|
+
"results": list(page.values("id", "name", "price")),
|
|
226
|
+
"total": total,
|
|
227
|
+
"has_more": (offset + limit) < total,
|
|
228
|
+
})
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## PDF Patterns
|
|
232
|
+
|
|
233
|
+
Pure functions, no classes. Take primitive data, return `bytes`. Internal
|
|
234
|
+
helpers prefixed with `_`:
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
import io
|
|
238
|
+
from reportlab.lib.pagesizes import A4
|
|
239
|
+
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle
|
|
240
|
+
|
|
241
|
+
def generate_restock_pdf(store_name: str, products_raw: list) -> bytes:
|
|
242
|
+
buffer = io.BytesIO()
|
|
243
|
+
doc = _make_doc(buffer)
|
|
244
|
+
story = _build_story(store_name, products_raw)
|
|
245
|
+
doc.build(story)
|
|
246
|
+
buffer.seek(0)
|
|
247
|
+
return buffer.read()
|
|
248
|
+
|
|
249
|
+
def _make_doc(buffer: io.BytesIO) -> SimpleDocTemplate:
|
|
250
|
+
return SimpleDocTemplate(
|
|
251
|
+
buffer,
|
|
252
|
+
pagesize=A4,
|
|
253
|
+
rightMargin=30,
|
|
254
|
+
leftMargin=30,
|
|
255
|
+
topMargin=30,
|
|
256
|
+
bottomMargin=30,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def _build_story(store_name: str, products_raw: list) -> list:
|
|
260
|
+
data = [["SKU", "Product", "Stock", "Min"]]
|
|
261
|
+
for p in products_raw:
|
|
262
|
+
data.append([p["sku"], p["name"], p["stock"], p["min_stock"]])
|
|
263
|
+
table = Table(data, colWidths=[60, 200, 50, 50])
|
|
264
|
+
table.setStyle(TableStyle([...]))
|
|
265
|
+
return [table]
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
The view wraps the result in `HttpResponse`:
|
|
269
|
+
|
|
270
|
+
```python
|
|
271
|
+
def api_restock_pdf(request):
|
|
272
|
+
products = Product.objects.filter(stock__lte=F("min_stock"))
|
|
273
|
+
pdf_bytes = generate_restock_pdf(request.store.name, list(products.values()))
|
|
274
|
+
response = HttpResponse(pdf_bytes, content_type="application/pdf")
|
|
275
|
+
response["Content-Disposition"] = 'inline; filename="restock.pdf"'
|
|
276
|
+
return response
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Cart Patterns
|
|
280
|
+
|
|
281
|
+
Two independent carts coexist per request. Session key is the only separator:
|
|
282
|
+
|
|
283
|
+
| Cart | Session Key | Module |
|
|
284
|
+
| ---------- | ----------- | ---------------------- |
|
|
285
|
+
| Storefront | `cart` | `apps/cart/cart.py` |
|
|
286
|
+
| POS | `pos_cart` | `apps/pos/pos_cart.py` |
|
|
287
|
+
|
|
288
|
+
Both follow the same contract:
|
|
289
|
+
|
|
290
|
+
- Initialize with `request`
|
|
291
|
+
- Store as `{str(product_id): {"quantity": int, "price": str}}`
|
|
292
|
+
- Price stored as `str` (Decimal is not JSON-serializable)
|
|
293
|
+
- Always call `self.session.modified = True` in `save()`
|
|
294
|
+
|
|
295
|
+
```python
|
|
296
|
+
class POSCart:
|
|
297
|
+
def __init__(self, request):
|
|
298
|
+
self.session = request.session.get("pos_cart", {})
|
|
299
|
+
|
|
300
|
+
def add(self, product_id, quantity):
|
|
301
|
+
key = str(product_id)
|
|
302
|
+
if key in self.session:
|
|
303
|
+
self.session[key]["quantity"] += quantity
|
|
304
|
+
else:
|
|
305
|
+
self.session[key] = {"quantity": quantity, "price": "0.00"}
|
|
306
|
+
self.save()
|
|
307
|
+
|
|
308
|
+
def save(self):
|
|
309
|
+
self.session["pos_cart"] = self.session
|
|
310
|
+
self.session.modified = True
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Type Hints
|
|
314
|
+
|
|
315
|
+
**Minimal**: Only on public service/utility function signatures, not on views or
|
|
316
|
+
helpers:
|
|
317
|
+
|
|
318
|
+
```python
|
|
319
|
+
# Service — annotate
|
|
320
|
+
def generate_restock_pdf(store_name: str, products_raw: list) -> bytes:
|
|
321
|
+
...
|
|
322
|
+
|
|
323
|
+
# View — no annotations
|
|
324
|
+
def api_cart_add(request):
|
|
325
|
+
...
|
|
326
|
+
|
|
327
|
+
# Private helpers — no annotations
|
|
328
|
+
def _build_cart_item(product, quantity):
|
|
329
|
+
...
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## Access Decorators
|
|
333
|
+
|
|
334
|
+
### require_pos_access
|
|
335
|
+
|
|
336
|
+
Verifies the employee has POS access:
|
|
337
|
+
|
|
338
|
+
```python
|
|
339
|
+
def require_pos_access(view):
|
|
340
|
+
def wrapper(request, *args, **kwargs):
|
|
341
|
+
if not request.user.is_authenticated:
|
|
342
|
+
return JsonResponse({"error": "Auth required"}, status=401)
|
|
343
|
+
if not request.user.is_superuser:
|
|
344
|
+
emp = EmployeeProfile.objects.get(user=request.user)
|
|
345
|
+
if not emp.has_pos_access:
|
|
346
|
+
return JsonResponse({"error": "No POS access"}, status=403)
|
|
347
|
+
return view(request, *args, **kwargs)
|
|
348
|
+
return wrapper
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### require_manager_access
|
|
352
|
+
|
|
353
|
+
Verifies the employee is a manager:
|
|
354
|
+
|
|
355
|
+
```python
|
|
356
|
+
def require_manager_access(view):
|
|
357
|
+
def wrapper(request, *args, **kwargs):
|
|
358
|
+
if not request.user.is_authenticated:
|
|
359
|
+
return JsonResponse({"error": "Auth required"}, status=401)
|
|
360
|
+
if not request.user.is_superuser:
|
|
361
|
+
emp = EmployeeProfile.objects.get(user=request.user)
|
|
362
|
+
if not emp.is_manager:
|
|
363
|
+
return JsonResponse({"error": "Manager required"}, status=403)
|
|
364
|
+
return view(request, *args, **kwargs)
|
|
365
|
+
return wrapper
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
## URLs and Routing
|
|
369
|
+
|
|
370
|
+
### URL Structure
|
|
371
|
+
|
|
372
|
+
```python
|
|
373
|
+
# apps/catalog/urls.py
|
|
374
|
+
from django.urls import path
|
|
375
|
+
|
|
376
|
+
urlpatterns = [
|
|
377
|
+
# API
|
|
378
|
+
path("api/products/", api_catalog_products, name="api_catalog_products"),
|
|
379
|
+
path("api/products/<int:pk>/", api_product_detail, name="api_product_detail"),
|
|
380
|
+
# Web
|
|
381
|
+
path("", product_list, name="catalog_product_list"),
|
|
382
|
+
path("<slug:slug>/", product_detail, name="catalog_product_detail"),
|
|
383
|
+
]
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Namespaces
|
|
387
|
+
|
|
388
|
+
```python
|
|
389
|
+
# config/urls.py
|
|
390
|
+
urlpatterns = [
|
|
391
|
+
path("catalog/", include("apps.catalog.urls", namespace="catalog")),
|
|
392
|
+
path("pos/", include("apps.pos.urls", namespace="pos")),
|
|
393
|
+
]
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
## Models — Best Practices
|
|
397
|
+
|
|
398
|
+
### Model Fields
|
|
399
|
+
|
|
400
|
+
```python
|
|
401
|
+
class Product(models.Model):
|
|
402
|
+
name = models.CharField(max_length=255)
|
|
403
|
+
slug = models.SlugField(max_length=255, unique=True)
|
|
404
|
+
description = models.TextField(blank=True)
|
|
405
|
+
price = models.DecimalField(max_digits=10, decimal_places=2)
|
|
406
|
+
stock = models.PositiveIntegerField(default=0)
|
|
407
|
+
is_active = models.BooleanField(default=True)
|
|
408
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
409
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
410
|
+
|
|
411
|
+
class Meta:
|
|
412
|
+
ordering = ["name"]
|
|
413
|
+
indexes = [
|
|
414
|
+
models.Index(fields=["is_active", "stock"]),
|
|
415
|
+
models.Index(fields=["slug"]),
|
|
416
|
+
]
|
|
417
|
+
|
|
418
|
+
def __str__(self):
|
|
419
|
+
return self.name
|
|
420
|
+
|
|
421
|
+
@property
|
|
422
|
+
def is_available(self):
|
|
423
|
+
return self.is_active and (self.is_service or self.stock > 0)
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### Choices
|
|
427
|
+
|
|
428
|
+
```python
|
|
429
|
+
class Order(models.Model):
|
|
430
|
+
class Status(models.TextChoices):
|
|
431
|
+
PENDING = "pending", "Pending"
|
|
432
|
+
PAID = "paid", "Paid"
|
|
433
|
+
SHIPPED = "shipped", "Shipped"
|
|
434
|
+
CANCELLED = "cancelled", "Cancelled"
|
|
435
|
+
|
|
436
|
+
status = models.CharField(
|
|
437
|
+
max_length=20,
|
|
438
|
+
choices=Status.choices,
|
|
439
|
+
default=Status.PENDING,
|
|
440
|
+
)
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
## Code Quality
|
|
444
|
+
|
|
445
|
+
### Line Length
|
|
446
|
+
|
|
447
|
+
88 characters (Black + Ruff). Run before every commit:
|
|
448
|
+
|
|
449
|
+
```bash
|
|
450
|
+
make format # black + ruff --fix
|
|
451
|
+
make lint # ruff check
|
|
452
|
+
uv run djlint . --reformat # templates
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
Pre-commit hooks enforce Black and Ruff automatically.
|
|
456
|
+
|
|
457
|
+
### Imports
|
|
458
|
+
|
|
459
|
+
```python
|
|
460
|
+
# Standard library
|
|
461
|
+
import os
|
|
462
|
+
import json
|
|
463
|
+
from datetime import datetime
|
|
464
|
+
|
|
465
|
+
# Third party
|
|
466
|
+
from django.db import models
|
|
467
|
+
from django.http import JsonResponse
|
|
468
|
+
from django.views.decorators.http import require_POST
|
|
469
|
+
|
|
470
|
+
# Local
|
|
471
|
+
from apps.catalog.models import Product
|
|
472
|
+
from apps.pos.services import CartService
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
## Commands
|
|
476
|
+
|
|
477
|
+
```bash
|
|
478
|
+
make run # dev server on 0.0.0.0:8000
|
|
479
|
+
make lint # ruff check
|
|
480
|
+
make format # black + ruff fix
|
|
481
|
+
make tests # pytest (--reuse-db --nomigrations by default)
|
|
482
|
+
make migrate # migrate_schemas --shared + --tenant
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
## Resources
|
|
486
|
+
|
|
487
|
+
- **FBV API examples**: `apps/pos/views.py`
|
|
488
|
+
- **Service layer**: `apps/orders/services.py`
|
|
489
|
+
- **PDF pattern**: `apps/catalog/reports.py`
|
|
490
|
+
- **Cart pattern**: `apps/pos/pos_cart.py`, `apps/cart/cart.py`
|
|
491
|
+
- **Pagination**: `apps/catalog/views.py` (api_catalog_products)
|
|
492
|
+
- **Django docs**: https://docs.djangoproject.com/en/5.2/
|