mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-02-05 05:25:18 -05:00
Compare commits
322 Commits
known_good
...
28c9ec56da
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28c9ec56da | ||
|
|
3ec5a4857d | ||
|
|
4da7e52fb0 | ||
|
|
b80654952d | ||
|
|
2b7bb4dfaa | ||
|
|
a801813dcf | ||
|
|
1c6e219662 | ||
|
|
70e4385c2b | ||
|
|
30aa887d2a | ||
|
|
dd2d09b1c7 | ||
|
|
89d9e945b9 | ||
|
|
bc4a3c7557 | ||
|
|
95700c7d7b | ||
|
|
1adba1b804 | ||
|
|
b243b17af7 | ||
|
|
c95f99ca10 | ||
|
|
aa56c46c27 | ||
|
|
137b9b8cb9 | ||
|
|
00699d53b4 | ||
|
|
cd8868a591 | ||
|
|
ed04b30469 | ||
|
|
a9f5644c5c | ||
|
|
a0be417f74 | ||
|
|
ca770d76ff | ||
|
|
edcd8f2076 | ||
|
|
ae31e889d7 | ||
|
|
2e35f8c5d9 | ||
|
|
45d97b6e68 | ||
|
|
b508434574 | ||
|
|
8f6acbdc23 | ||
|
|
b860e332cb | ||
|
|
7ba0004c93 | ||
|
|
b9063ff4f8 | ||
|
|
bf04e4d854 | ||
|
|
1b246eeaa4 | ||
|
|
fdbbca2add | ||
|
|
bf365693f8 | ||
|
|
42a3dc7637 | ||
|
|
209b433577 | ||
|
|
01195e198c | ||
|
|
a5fd56b117 | ||
|
|
6ce2c30065 | ||
|
|
cd6403615f | ||
|
|
6625fb5ba9 | ||
|
|
d5cd6ad0a3 | ||
|
|
516c847377 | ||
|
|
c2c26cfd1d | ||
|
|
61d73a2147 | ||
|
|
0febfdef2f | ||
|
|
f769faed60 | ||
|
|
3d4115a108 | ||
|
|
35f8d0ef8f | ||
|
|
0fd6dc2560 | ||
|
|
91906e0d57 | ||
|
|
5bf351fd2b | ||
|
|
49f874f7b4 | ||
|
|
9bed782784 | ||
|
|
fb6726f89a | ||
|
|
04394b9976 | ||
|
|
bb7da85516 | ||
|
|
7b9f64be72 | ||
|
|
ac745cc541 | ||
|
|
02ac587216 | ||
|
|
67db0aa46e | ||
|
|
715e284b3e | ||
|
|
08a4a2d034 | ||
|
|
6125c4ee44 | ||
|
|
53b63d5f09 | ||
|
|
97892e4fc9 | ||
|
|
133dcabb58 | ||
|
|
b627aed65d | ||
|
|
e4e36c7899 | ||
|
|
831be6a2ee | ||
|
|
bf7e0c0f40 | ||
|
|
dcf890a55c | ||
|
|
937eee19e4 | ||
|
|
e62646bcf9 | ||
|
|
92f4104d7a | ||
|
|
02c7cbd1cd | ||
|
|
d504d41de2 | ||
|
|
b0e0678590 | ||
|
|
652ea149bd | ||
|
|
66ed4347a9 | ||
|
|
69c07d1381 | ||
|
|
bead0654df | ||
|
|
37a20f83ba | ||
|
|
2304085c32 | ||
|
|
31d83c8889 | ||
|
|
46c6e45eae | ||
|
|
f5db23a791 | ||
|
|
78248aa892 | ||
|
|
641fc1a253 | ||
|
|
ca7555c052 | ||
|
|
74b45aa143 | ||
|
|
d9fc13f350 | ||
|
|
f4f8ec8f9b | ||
|
|
274ba650b3 | ||
|
|
cc990ee003 | ||
|
|
63b9cf1a70 | ||
|
|
c26414ff74 | ||
|
|
17228e9935 | ||
|
|
32736ae660 | ||
|
|
b5bae44cb8 | ||
|
|
da7c7e3381 | ||
|
|
f6c8e0e25c | ||
|
|
16386deee7 | ||
|
|
7815de158e | ||
|
|
b871a1d396 | ||
|
|
751cd86a31 | ||
|
|
8360f3fd43 | ||
|
|
b570cb6848 | ||
|
|
94736acdd5 | ||
|
|
6781fa3564 | ||
|
|
4b11ec112e | ||
|
|
de05a5abda | ||
|
|
401449201c | ||
|
|
1ca1362fee | ||
|
|
02e4b82beb | ||
|
|
4339c5c5e0 | ||
|
|
5278ad39d0 | ||
|
|
4d145ebabe | ||
|
|
e4959b7a04 | ||
|
|
ef2437b7f4 | ||
|
|
3523274cbd | ||
|
|
d7951756dc | ||
|
|
518fcbee22 | ||
|
|
41f1738cc1 | ||
|
|
645a74a4c3 | ||
|
|
8c85b2afd4 | ||
|
|
063398d220 | ||
|
|
20ae4862e4 | ||
|
|
5541a5f02d | ||
|
|
78f465b273 | ||
|
|
0b51ee123a | ||
|
|
c19aaf2f4b | ||
|
|
9d6f6dab2c | ||
|
|
bba707fa98 | ||
|
|
c197051b25 | ||
|
|
1fe299fb4b | ||
|
|
af57592496 | ||
|
|
62723d0e33 | ||
|
|
f5c063b76f | ||
|
|
59efc39143 | ||
|
|
2e0d32819a | ||
|
|
6034227796 | ||
|
|
fb6c6ec37b | ||
|
|
99b935da19 | ||
|
|
2756079010 | ||
|
|
5195c234c6 | ||
|
|
c861d4f6ae | ||
|
|
ac71e5f047 | ||
|
|
bdbb864cef | ||
|
|
b46e13426a | ||
|
|
39c8fe2c57 | ||
|
|
c27f320a49 | ||
|
|
f4c6cd99db | ||
|
|
467f7ba3f8 | ||
|
|
d4a1f88644 | ||
|
|
369c5e698e | ||
|
|
19b7aee707 | ||
|
|
a09fd66d70 | ||
|
|
910762722e | ||
|
|
79e34473a4 | ||
|
|
872f3378a1 | ||
|
|
df91eb97b8 | ||
|
|
ad33332506 | ||
|
|
69cdb0a554 | ||
|
|
d2b6b712bf | ||
|
|
1784644a52 | ||
|
|
3c40a32925 | ||
|
|
29392f0de1 | ||
|
|
d0bd0e1bf9 | ||
|
|
11e643a47a | ||
|
|
db78de4cfe | ||
|
|
4a495182bd | ||
|
|
2add4c7fc2 | ||
|
|
f1c37f2bc1 | ||
|
|
1c71ad9b6b | ||
|
|
7e8c40db0d | ||
|
|
7211c17aae | ||
|
|
a16b0444d4 | ||
|
|
a01cda306e | ||
|
|
808deb82e2 | ||
|
|
2d2d832e07 | ||
|
|
b4c474c496 | ||
|
|
9ed28b15b4 | ||
|
|
4b32580b13 | ||
|
|
228eeeb3c8 | ||
|
|
b7f6c60682 | ||
|
|
7ecf43f1a4 | ||
|
|
a148d34cf9 | ||
|
|
71b73522ae | ||
|
|
03f9df4bab | ||
|
|
75f5b07129 | ||
|
|
86ae24bbac | ||
|
|
0e0ed01cee | ||
|
|
2c4d2daf34 | ||
|
|
d353f24f9d | ||
|
|
9c65df12bb | ||
|
|
ecf94bf84e | ||
|
|
f3d28817a5 | ||
|
|
6fa807f4b6 | ||
|
|
323aa561a5 | ||
|
|
7d25d6f992 | ||
|
|
19852207f6 | ||
|
|
185af7fd17 | ||
|
|
768f05b783 | ||
|
|
411c6f6f68 | ||
|
|
789a6386a5 | ||
|
|
dbd76785b5 | ||
|
|
4215e14b5e | ||
|
|
dee7c61320 | ||
|
|
2f26061170 | ||
|
|
bc68eaf4d9 | ||
|
|
1ef38f4a96 | ||
|
|
1f3f94702e | ||
|
|
63b484b724 | ||
|
|
1182e894e3 | ||
|
|
cda755ea59 | ||
|
|
8bbfce3f2a | ||
|
|
0e97fdc96b | ||
|
|
ebc38228e6 | ||
|
|
45c40c720d | ||
|
|
696d26acdd | ||
|
|
96857ad1d4 | ||
|
|
ef40184e07 | ||
|
|
7aa706d12a | ||
|
|
de6146f812 | ||
|
|
1bfbe4a8b4 | ||
|
|
209c3e4d21 | ||
|
|
886b275f65 | ||
|
|
c9ebf4c833 | ||
|
|
672749d109 | ||
|
|
a5c3e56046 | ||
|
|
d728ba6e9c | ||
|
|
f819a1f07c | ||
|
|
d91f79e29c | ||
|
|
304812d43f | ||
|
|
3f7296d7a5 | ||
|
|
e60f73de9d | ||
|
|
af7ea6b4ce | ||
|
|
36478c7a1b | ||
|
|
c8628984e0 | ||
|
|
1bfe08a0a7 | ||
|
|
901a1c421d | ||
|
|
280ad4d6da | ||
|
|
9634bac155 | ||
|
|
69094a9af8 | ||
|
|
d338917ca1 | ||
|
|
5acc74d34c | ||
|
|
8b7ad53cbd | ||
|
|
7553752f0d | ||
|
|
1ca84208ef | ||
|
|
fdd7a4fcf1 | ||
|
|
ae8710c157 | ||
|
|
56d9174bb5 | ||
|
|
5c62b41070 | ||
|
|
8014bcc368 | ||
|
|
1a60658f17 | ||
|
|
311c0e999c | ||
|
|
6af9f7332a | ||
|
|
7c81d8e8eb | ||
|
|
04daf9573b | ||
|
|
96e2f097cd | ||
|
|
10dfa20d65 | ||
|
|
d514687651 | ||
|
|
6e6558d51a | ||
|
|
8e9aabb2ab | ||
|
|
7365a23205 | ||
|
|
86059bbcba | ||
|
|
43ec3efa68 | ||
|
|
addb9511da | ||
|
|
dfe6194039 | ||
|
|
e63677e8c0 | ||
|
|
af3e255a2e | ||
|
|
ebbf772669 | ||
|
|
891c29beff | ||
|
|
e86cb95f14 | ||
|
|
fd97ed31cb | ||
|
|
d9990ae241 | ||
|
|
da0ee1acfa | ||
|
|
30b786d51e | ||
|
|
8d70bf8994 | ||
|
|
edc9d66849 | ||
|
|
8265348a83 | ||
|
|
8f7f7add2d | ||
|
|
7ec4d964dc | ||
|
|
d68c927a00 | ||
|
|
caba5c6158 | ||
|
|
131ef7ceb0 | ||
|
|
a30f3ef644 | ||
|
|
2e1040e3a6 | ||
|
|
751d21098d | ||
|
|
1acfe9d29e | ||
|
|
6a9154ce69 | ||
|
|
15e56c9770 | ||
|
|
09ee45f6c7 | ||
|
|
177117f4d6 | ||
|
|
96341bfd82 | ||
|
|
f011d58c6d | ||
|
|
983c101ed1 | ||
|
|
97a3555e81 | ||
|
|
ec626b4124 | ||
|
|
537ea0fc07 | ||
|
|
be07a17460 | ||
|
|
1c03e4acb8 | ||
|
|
a5ebeb51dc | ||
|
|
1ee4b00961 | ||
|
|
5a1fdb6d16 | ||
|
|
78355c60f9 | ||
|
|
d8a65f4e81 | ||
|
|
cac6335bb7 | ||
|
|
7f4de7c2ec | ||
|
|
08e97f21b7 | ||
|
|
9ee380c3ea | ||
|
|
d2c9d02523 | ||
|
|
75287c8994 | ||
|
|
d7a89d8725 | ||
|
|
ff52f182fc | ||
|
|
33b7c1a81a | ||
|
|
8b93f67e31 | ||
|
|
9df5ab931f |
73
.agent/MEMORY/migration_source.md
Normal file
73
.agent/MEMORY/migration_source.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Migration Source: thrillwiki-87
|
||||
|
||||
The React project at `/Volumes/macminissd/Projects/thrillwiki-87` is the **authoritative source** for ThrillWiki features and functionality.
|
||||
|
||||
## Core Principle
|
||||
|
||||
**thrillwiki-87 is LAW.** When migrating features, the React implementation defines:
|
||||
- What features must exist
|
||||
- How they should behave
|
||||
- What data structures are required
|
||||
- What UI patterns to follow
|
||||
|
||||
## Source Project Structure
|
||||
|
||||
```
|
||||
thrillwiki-87/
|
||||
├── src/
|
||||
│ ├── components/ # React components (44 directories)
|
||||
│ ├── pages/ # Route pages (39 files)
|
||||
│ ├── hooks/ # React hooks (80+ files)
|
||||
│ ├── types/ # TypeScript definitions (63 files)
|
||||
│ ├── contexts/ # React contexts
|
||||
│ ├── lib/ # Utilities
|
||||
│ └── integrations/ # External service integrations
|
||||
├── docs/ # Feature documentation (78 files)
|
||||
│ ├── SITE_OVERVIEW.md
|
||||
│ ├── DESIGN_SYSTEM.md
|
||||
│ ├── COMPONENTS.md
|
||||
│ ├── PAGES.md
|
||||
│ └── USER_FLOWS.md
|
||||
└── supabase/ # Backend schemas and functions
|
||||
```
|
||||
|
||||
## Technology Translation
|
||||
|
||||
| React (Source) | Nuxt 4 (Target) |
|
||||
|----------------|-----------------|
|
||||
| React component | Vue SFC (.vue) |
|
||||
| useState | ref() / reactive() |
|
||||
| useEffect | watch() / onMounted() |
|
||||
| useContext | provide() / inject() or Pinia |
|
||||
| React Router | Nuxt file-based routing |
|
||||
| React Query | useAsyncData / useFetch |
|
||||
| shadcn-ui | Nuxt UI |
|
||||
| Supabase client | Django REST API via useApi() |
|
||||
| Edge Functions | Django views |
|
||||
|
||||
## Backend Translation
|
||||
|
||||
| Supabase (Source) | Django (Target) |
|
||||
|-------------------|-----------------|
|
||||
| Table | Django Model |
|
||||
| RLS policies | DRF permissions |
|
||||
| Edge Functions | Django views/viewsets |
|
||||
| Realtime | SSE / WebSockets |
|
||||
| Auth | django-allauth + JWT |
|
||||
| Storage | Cloudflare R2 |
|
||||
|
||||
## Migration Workflow
|
||||
|
||||
1. **Find source** in thrillwiki-87
|
||||
2. **Read the docs** in thrillwiki-87/docs/
|
||||
3. **Check existing** Nuxt implementation
|
||||
4. **Port missing features** to achieve parity
|
||||
5. **Verify behavior** matches source
|
||||
|
||||
## Key Source Files to Reference
|
||||
|
||||
When porting a feature, always check:
|
||||
- `thrillwiki-87/docs/` for specifications
|
||||
- `thrillwiki-87/src/types/` for data structures
|
||||
- `thrillwiki-87/src/hooks/` for business logic
|
||||
- `thrillwiki-87/src/components/` for UI patterns
|
||||
83
.agent/MEMORY/source_mapping.md
Normal file
83
.agent/MEMORY/source_mapping.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Source Mapping: React → Nuxt
|
||||
|
||||
Quick reference for mapping thrillwiki-87 paths to thrillwiki_django_no_react paths.
|
||||
|
||||
## Directory Mappings
|
||||
|
||||
| React (thrillwiki-87) | Nuxt (thrillwiki_django_no_react) |
|
||||
|----------------------|-----------------------------------|
|
||||
| `src/components/` | `frontend/app/components/` |
|
||||
| `src/pages/` | `frontend/app/pages/` |
|
||||
| `src/hooks/` | `frontend/app/composables/` |
|
||||
| `src/types/` | `frontend/app/types/` |
|
||||
| `src/lib/` | `frontend/app/utils/` |
|
||||
| `src/contexts/` | `frontend/app/stores/` (Pinia) |
|
||||
| `docs/` | `source_docs/` |
|
||||
| `supabase/migrations/` | `backend/apps/*/models.py` |
|
||||
|
||||
## Component Mappings (shadcn-ui → Nuxt UI)
|
||||
|
||||
| shadcn-ui | Nuxt UI |
|
||||
|-----------|---------|
|
||||
| `<Button>` | `<UButton>` |
|
||||
| `<Card>` | `<UCard>` |
|
||||
| `<Dialog>` | `<UModal>` |
|
||||
| `<Input>` | `<UInput>` |
|
||||
| `<Select>` | `<USelect>` / `<USelectMenu>` |
|
||||
| `<Tabs>` | `<UTabs>` |
|
||||
| `<Table>` | `<UTable>` |
|
||||
| `<Badge>` | `<UBadge>` |
|
||||
| `<Avatar>` | `<UAvatar>` |
|
||||
| `<Tooltip>` | `<UTooltip>` |
|
||||
| `<Sheet>` | `<USlideover>` |
|
||||
| `<AlertDialog>` | `<UModal>` + confirm pattern |
|
||||
| `<Skeleton>` | `<USkeleton>` |
|
||||
| `<Textarea>` | `<UTextarea>` |
|
||||
| `<Checkbox>` | `<UCheckbox>` |
|
||||
| `<RadioGroup>` | `<URadioGroup>` |
|
||||
| `<Switch>` | `<UToggle>` |
|
||||
| `<DropdownMenu>` | `<UDropdown>` |
|
||||
| `<Command>` | `<UCommandPalette>` |
|
||||
| `<Popover>` | `<UPopover>` |
|
||||
|
||||
## Page Mappings
|
||||
|
||||
| React Page | Nuxt Page |
|
||||
|------------|-----------|
|
||||
| `Index.tsx` | `pages/index.vue` |
|
||||
| `Parks.tsx` | `pages/parks/index.vue` |
|
||||
| `ParkDetail.tsx` | `pages/parks/[park_slug]/index.vue` |
|
||||
| `Rides.tsx` | `pages/rides/index.vue` |
|
||||
| `RideDetail.tsx` | `pages/parks/[park_slug]/rides/[ride_slug].vue` |
|
||||
| `Manufacturers.tsx` | `pages/manufacturers/index.vue` |
|
||||
| `ManufacturerDetail.tsx` | `pages/manufacturers/[slug].vue` |
|
||||
| `Designers.tsx` | `pages/designers/index.vue` |
|
||||
| `DesignerDetail.tsx` | `pages/designers/[slug].vue` |
|
||||
| `Operators.tsx` | `pages/operators/index.vue` |
|
||||
| `OperatorDetail.tsx` | `pages/operators/[slug].vue` |
|
||||
| `ParkOwners.tsx` | `pages/owners/index.vue` |
|
||||
| `PropertyOwnerDetail.tsx` | `pages/owners/[slug].vue` |
|
||||
| `Auth.tsx` | `pages/auth/login.vue`, `pages/auth/signup.vue` |
|
||||
| `Profile.tsx` | `pages/profile/index.vue` |
|
||||
| `Search.tsx` | `pages/search.vue` |
|
||||
| `AdminDashboard.tsx` | `pages/admin/index.vue` |
|
||||
|
||||
## Hook → Composable Mappings
|
||||
|
||||
| React Hook | Vue Composable |
|
||||
|------------|----------------|
|
||||
| `useAuth.tsx` | `useAuth.ts` |
|
||||
| `useSearch.tsx` | `useSearchHistory.ts` |
|
||||
| `useModerationQueue.ts` | `useModeration.ts` |
|
||||
| `useProfile.tsx` | (inline in pages) |
|
||||
| `useLocations.ts` | `useParksApi.ts` |
|
||||
| `useUnitPreferences.ts` | `useUnits.ts` |
|
||||
|
||||
## API Endpoint Translation
|
||||
|
||||
| Supabase RPC/Query | Django API |
|
||||
|--------------------|------------|
|
||||
| `supabase.from('parks')` | `GET /api/v1/parks/` |
|
||||
| `supabase.rpc('search_*')` | `GET /api/v1/search/` |
|
||||
| `supabase.auth.*` | `/api/v1/auth/*` |
|
||||
| Edge Functions | Django views in `backend/apps/*/views.py` |
|
||||
329
.agent/rules/api-conventions.md
Normal file
329
.agent/rules/api-conventions.md
Normal file
@@ -0,0 +1,329 @@
|
||||
# ThrillWiki API Conventions
|
||||
|
||||
Standards for designing and implementing APIs between the Django backend and Nuxt frontend.
|
||||
|
||||
## API Base Structure
|
||||
|
||||
### URL Patterns
|
||||
```
|
||||
/api/v1/ # API root (versioned)
|
||||
/api/v1/parks/ # List/Create parks
|
||||
/api/v1/parks/{slug}/ # Retrieve/Update/Delete park
|
||||
/api/v1/parks/{slug}/rides/ # Nested resource
|
||||
/api/v1/auth/ # Authentication endpoints
|
||||
/api/v1/users/me/ # Current user
|
||||
```
|
||||
|
||||
### HTTP Methods
|
||||
| Method | Usage |
|
||||
|--------|-------|
|
||||
| GET | Retrieve resource(s) |
|
||||
| POST | Create resource |
|
||||
| PUT | Replace resource entirely |
|
||||
| PATCH | Update resource partially |
|
||||
| DELETE | Remove resource |
|
||||
|
||||
## Response Formats
|
||||
|
||||
### Success Response (Single Resource)
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"name": "Cedar Point",
|
||||
"slug": "cedar-point",
|
||||
"created_at": "2024-01-15T10:30:00Z",
|
||||
"updated_at": "2024-01-16T14:20:00Z",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Success Response (List)
|
||||
```json
|
||||
{
|
||||
"count": 150,
|
||||
"next": "/api/v1/parks/?page=2",
|
||||
"previous": null,
|
||||
"results": [
|
||||
{ ... },
|
||||
{ ... }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Error Response
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "validation_error",
|
||||
"message": "Invalid input data",
|
||||
"details": {
|
||||
"name": ["This field is required."],
|
||||
"status": ["Invalid choice."]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### HTTP Status Codes
|
||||
| Code | Meaning | When to Use |
|
||||
|------|---------|-------------|
|
||||
| 200 | OK | Successful GET, PUT, PATCH |
|
||||
| 201 | Created | Successful POST |
|
||||
| 204 | No Content | Successful DELETE |
|
||||
| 400 | Bad Request | Validation errors |
|
||||
| 401 | Unauthorized | Missing/invalid auth |
|
||||
| 403 | Forbidden | Insufficient permissions |
|
||||
| 404 | Not Found | Resource doesn't exist |
|
||||
| 409 | Conflict | Duplicate resource |
|
||||
| 500 | Server Error | Unexpected errors |
|
||||
|
||||
## Pagination
|
||||
|
||||
### Query Parameters
|
||||
```
|
||||
?page=2 # Page number (default: 1)
|
||||
?page_size=20 # Items per page (default: 20, max: 100)
|
||||
```
|
||||
|
||||
### Response
|
||||
```json
|
||||
{
|
||||
"count": 150,
|
||||
"next": "/api/v1/parks/?page=3",
|
||||
"previous": "/api/v1/parks/?page=1",
|
||||
"results": [ ... ]
|
||||
}
|
||||
```
|
||||
|
||||
## Filtering & Sorting
|
||||
|
||||
### Filtering
|
||||
```
|
||||
GET /api/v1/parks/?status=operating
|
||||
GET /api/v1/parks/?country=USA&status=operating
|
||||
GET /api/v1/rides/?park=cedar-point&type=coaster
|
||||
```
|
||||
|
||||
### Search
|
||||
```
|
||||
GET /api/v1/parks/?search=cedar
|
||||
```
|
||||
|
||||
### Sorting
|
||||
```
|
||||
GET /api/v1/parks/?ordering=name # Ascending
|
||||
GET /api/v1/parks/?ordering=-created_at # Descending
|
||||
GET /api/v1/parks/?ordering=-rating,name # Multiple fields
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
### Token-Based Auth
|
||||
```
|
||||
Authorization: Bearer <jwt_token>
|
||||
```
|
||||
|
||||
### Endpoints
|
||||
```
|
||||
POST /api/v1/auth/login/ # Get tokens
|
||||
POST /api/v1/auth/register/ # Create account
|
||||
POST /api/v1/auth/refresh/ # Refresh access token
|
||||
POST /api/v1/auth/logout/ # Invalidate tokens
|
||||
GET /api/v1/auth/me/ # Current user info
|
||||
```
|
||||
|
||||
### Social Auth
|
||||
```
|
||||
POST /api/v1/auth/google/ # Google OAuth
|
||||
POST /api/v1/auth/discord/ # Discord OAuth
|
||||
```
|
||||
|
||||
## Content Submission API
|
||||
|
||||
### Submit New Content
|
||||
```
|
||||
POST /api/v1/submissions/
|
||||
{
|
||||
"content_type": "park",
|
||||
"data": {
|
||||
"name": "New Park Name",
|
||||
"city": "Orlando",
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Response
|
||||
```json
|
||||
{
|
||||
"id": "submission-uuid",
|
||||
"status": "pending",
|
||||
"content_type": "park",
|
||||
"data": { ... },
|
||||
"created_at": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Submit Edit to Existing Content
|
||||
```
|
||||
POST /api/v1/submissions/
|
||||
{
|
||||
"content_type": "park",
|
||||
"object_id": "existing-park-uuid",
|
||||
"data": {
|
||||
"description": "Updated description..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Moderation API
|
||||
|
||||
### Get Moderation Queue
|
||||
```
|
||||
GET /api/v1/moderation/
|
||||
GET /api/v1/moderation/?status=pending&type=park
|
||||
```
|
||||
|
||||
### Review Submission
|
||||
```
|
||||
POST /api/v1/moderation/{id}/approve/
|
||||
POST /api/v1/moderation/{id}/reject/
|
||||
{
|
||||
"notes": "Reason for rejection..."
|
||||
}
|
||||
```
|
||||
|
||||
## Nested Resources
|
||||
|
||||
### Pattern
|
||||
```
|
||||
/api/v1/parks/{slug}/rides/ # List rides in park
|
||||
/api/v1/parks/{slug}/reviews/ # List park reviews
|
||||
/api/v1/parks/{slug}/photos/ # List park photos
|
||||
```
|
||||
|
||||
### When to Nest vs Flat
|
||||
**Nest when:**
|
||||
- Resource only makes sense in context of parent (park photos)
|
||||
- Need to filter by parent frequently
|
||||
|
||||
**Use flat with filter when:**
|
||||
- Resource can exist independently
|
||||
- Need to query across parents
|
||||
|
||||
```
|
||||
# Flat with filter
|
||||
GET /api/v1/rides/?park=cedar-point
|
||||
|
||||
# Or nested
|
||||
GET /api/v1/parks/cedar-point/rides/
|
||||
```
|
||||
|
||||
## Geolocation API
|
||||
|
||||
### Parks Nearby
|
||||
```
|
||||
GET /api/v1/parks/nearby/?lat=41.4821&lng=-82.6822&radius=50
|
||||
```
|
||||
|
||||
### Response includes distance
|
||||
```json
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"name": "Cedar Point",
|
||||
"distance": 12.5, // in user's preferred units
|
||||
...
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## File Uploads
|
||||
|
||||
### Photo Upload
|
||||
```
|
||||
POST /api/v1/photos/
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
{
|
||||
"content_type": "park",
|
||||
"object_id": "park-uuid",
|
||||
"image": <file>,
|
||||
"caption": "Optional caption"
|
||||
}
|
||||
```
|
||||
|
||||
### Response
|
||||
```json
|
||||
{
|
||||
"id": "photo-uuid",
|
||||
"url": "https://cdn.thrillwiki.com/photos/...",
|
||||
"thumbnail_url": "https://cdn.thrillwiki.com/photos/.../thumb",
|
||||
"status": "pending", // Pending moderation
|
||||
"created_at": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
### Headers
|
||||
```
|
||||
X-RateLimit-Limit: 100
|
||||
X-RateLimit-Remaining: 95
|
||||
X-RateLimit-Reset: 1642089600
|
||||
```
|
||||
|
||||
### When Limited
|
||||
```
|
||||
HTTP/1.1 429 Too Many Requests
|
||||
{
|
||||
"error": {
|
||||
"code": "rate_limit_exceeded",
|
||||
"message": "Too many requests. Try again in 60 seconds."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Frontend Integration
|
||||
|
||||
### API Client Setup (Nuxt)
|
||||
```typescript
|
||||
// composables/useApi.ts
|
||||
export function useApi() {
|
||||
const config = useRuntimeConfig()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const api = $fetch.create({
|
||||
baseURL: config.public.apiBase,
|
||||
headers: authStore.token ? {
|
||||
Authorization: `Bearer ${authStore.token}`
|
||||
} : {},
|
||||
onResponseError: ({ response }) => {
|
||||
if (response.status === 401) {
|
||||
authStore.logout()
|
||||
navigateTo('/auth/login')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return api
|
||||
}
|
||||
```
|
||||
|
||||
### Usage in Components
|
||||
```typescript
|
||||
const api = useApi()
|
||||
|
||||
// Fetch parks
|
||||
const { data: parks } = await useAsyncData('parks', () =>
|
||||
api('/parks/', { params: { status: 'operating' } })
|
||||
)
|
||||
|
||||
// Create submission
|
||||
await api('/submissions/', {
|
||||
method: 'POST',
|
||||
body: { content_type: 'park', data: formData }
|
||||
})
|
||||
```
|
||||
306
.agent/rules/component-patterns.md
Normal file
306
.agent/rules/component-patterns.md
Normal file
@@ -0,0 +1,306 @@
|
||||
# ThrillWiki Component Patterns
|
||||
|
||||
Guidelines for building UI components consistent with ThrillWiki's design system.
|
||||
|
||||
## Component Hierarchy
|
||||
|
||||
```
|
||||
components/
|
||||
├── layout/ # Page structure
|
||||
│ ├── Header.vue
|
||||
│ ├── Footer.vue
|
||||
│ ├── Sidebar.vue
|
||||
│ └── PageContainer.vue
|
||||
├── ui/ # Base components (shadcn/ui style)
|
||||
│ ├── Button.vue
|
||||
│ ├── Card.vue
|
||||
│ ├── Input.vue
|
||||
│ ├── Badge.vue
|
||||
│ ├── Avatar.vue
|
||||
│ ├── Modal.vue
|
||||
│ └── ...
|
||||
├── entity/ # Domain-specific cards
|
||||
│ ├── ParkCard.vue
|
||||
│ ├── RideCard.vue
|
||||
│ ├── ReviewCard.vue
|
||||
│ ├── CreditCard.vue
|
||||
│ └── CompanyCard.vue
|
||||
├── forms/ # Form components
|
||||
│ ├── ParkForm.vue
|
||||
│ ├── ReviewForm.vue
|
||||
│ └── ...
|
||||
└── specialty/ # Complex/unique components
|
||||
├── SearchAutocomplete.vue
|
||||
├── Map.vue
|
||||
├── ImageGallery.vue
|
||||
├── UnitDisplay.vue
|
||||
└── RatingDisplay.vue
|
||||
```
|
||||
|
||||
## Base Components (ui/)
|
||||
|
||||
### Card
|
||||
```vue
|
||||
<template>
|
||||
<div :class="[
|
||||
'rounded-lg border bg-card text-card-foreground',
|
||||
interactive && 'hover:shadow-md transition-shadow cursor-pointer',
|
||||
className
|
||||
]">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
interactive?: boolean
|
||||
className?: string
|
||||
}>()
|
||||
</script>
|
||||
```
|
||||
|
||||
### Button
|
||||
```vue
|
||||
<template>
|
||||
<button :class="[buttonVariants({ variant, size }), className]">
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
variant?: 'default' | 'secondary' | 'outline' | 'ghost' | 'destructive'
|
||||
size?: 'default' | 'sm' | 'lg' | 'icon'
|
||||
className?: string
|
||||
}>()
|
||||
</script>
|
||||
```
|
||||
|
||||
### Badge
|
||||
```vue
|
||||
<template>
|
||||
<span :class="[badgeVariants({ variant }), className]">
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
variant?: 'default' | 'secondary' | 'success' | 'warning' | 'destructive' | 'outline'
|
||||
className?: string
|
||||
}>()
|
||||
</script>
|
||||
```
|
||||
|
||||
## Entity Cards
|
||||
|
||||
### ParkCard
|
||||
Displays park preview with image, name, location, stats, and status.
|
||||
|
||||
**Required Props:**
|
||||
- `park: Park` - Park object
|
||||
|
||||
**Displays:**
|
||||
- Park image (with fallback)
|
||||
- Park name
|
||||
- Location (city, country)
|
||||
- Ride count
|
||||
- Average rating
|
||||
- Status badge (Operating/Closed/Under Construction)
|
||||
|
||||
**Interactions:**
|
||||
- Click navigates to park detail page
|
||||
- Hover shows elevation shadow
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<NuxtLink :to="`/parks/${park.slug}`">
|
||||
<Card interactive>
|
||||
<div class="aspect-video relative overflow-hidden rounded-t-lg">
|
||||
<NuxtImg
|
||||
:src="park.image || '/placeholder-park.jpg'"
|
||||
:alt="park.name"
|
||||
class="object-cover w-full h-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<h3 class="font-semibold text-lg line-clamp-1">{{ park.name }}</h3>
|
||||
<p class="text-sm text-muted-foreground flex items-center gap-1">
|
||||
<MapPin class="w-4 h-4" />
|
||||
{{ park.city }}, {{ park.country }}
|
||||
</p>
|
||||
<div class="flex items-center justify-between mt-2">
|
||||
<span class="text-sm">🎢 {{ park.rideCount }} rides</span>
|
||||
<RatingDisplay :rating="park.averageRating" size="sm" />
|
||||
</div>
|
||||
<Badge :variant="statusVariant" class="mt-2">
|
||||
{{ park.status }}
|
||||
</Badge>
|
||||
</div>
|
||||
</Card>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
```
|
||||
|
||||
### RideCard
|
||||
Similar structure to ParkCard, but shows:
|
||||
- Ride image
|
||||
- Ride name
|
||||
- Park name (linked)
|
||||
- Key specs (speed, height)
|
||||
- Type badge + Status badge
|
||||
|
||||
### ReviewCard
|
||||
Displays user review with:
|
||||
- User avatar + username
|
||||
- Rating (5-star display)
|
||||
- Review date (relative)
|
||||
- Review text
|
||||
- Helpful votes (👍 count)
|
||||
- Actions (Reply, Report)
|
||||
|
||||
### CreditCard
|
||||
For user's ride credit list:
|
||||
- Ride thumbnail
|
||||
- Ride name + park name
|
||||
- Ride count with +/- controls
|
||||
- Last ridden date
|
||||
- Edit button
|
||||
|
||||
## Specialty Components
|
||||
|
||||
### SearchAutocomplete
|
||||
Global search with instant results.
|
||||
|
||||
**Features:**
|
||||
- Debounced input (300ms)
|
||||
- Results grouped by type (Parks, Rides, Companies)
|
||||
- Keyboard navigation
|
||||
- Click or Enter to navigate
|
||||
- Empty state handling
|
||||
|
||||
**Implementation Notes:**
|
||||
- Use `useFetch` with `watch` for reactive searching
|
||||
- Show loading skeleton while fetching
|
||||
- Limit results to 10 per category
|
||||
- Highlight matching text
|
||||
|
||||
### UnitDisplay
|
||||
Converts and displays values in user's preferred units.
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<span>{{ formattedValue }}</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
value: number
|
||||
type: 'speed' | 'height' | 'length' | 'weight'
|
||||
showBoth?: boolean // Show both metric and imperial
|
||||
}>()
|
||||
|
||||
const { preferredUnits } = useUnits()
|
||||
|
||||
const formattedValue = computed(() => {
|
||||
// Convert and format based on type and preference
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### RatingDisplay
|
||||
Star rating visualization.
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="flex">
|
||||
<Star
|
||||
v-for="i in 5"
|
||||
:key="i"
|
||||
:class="[
|
||||
'w-4 h-4',
|
||||
i <= Math.round(rating) ? 'fill-yellow-400 text-yellow-400' : 'text-gray-300'
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
<span class="text-sm font-medium">{{ rating.toFixed(1) }}</span>
|
||||
<span v-if="count" class="text-sm text-muted-foreground">({{ count }})</span>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Map (Leaflet)
|
||||
Interactive map for parks nearby feature.
|
||||
|
||||
**Features:**
|
||||
- Marker clusters for dense areas
|
||||
- Custom markers for different park types
|
||||
- Popup on marker click with park preview
|
||||
- Zoom controls
|
||||
- Full-screen toggle
|
||||
- User location marker (if permitted)
|
||||
|
||||
## Form Components
|
||||
|
||||
### Standard Form Structure
|
||||
```vue
|
||||
<template>
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="space-y-4">
|
||||
<FormField label="Name" :error="errors.name">
|
||||
<Input v-model="form.name" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="Description">
|
||||
<Textarea v-model="form.description" />
|
||||
</FormField>
|
||||
|
||||
<div class="flex gap-2 justify-end">
|
||||
<Button variant="outline" @click="$emit('cancel')">Cancel</Button>
|
||||
<Button type="submit" :loading="isSubmitting">Save</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Validation
|
||||
- Use Zod for schema validation
|
||||
- Display errors inline below fields
|
||||
- Disable submit button while invalid
|
||||
- Show loading state during submission
|
||||
|
||||
## Loading States
|
||||
|
||||
### Skeleton Loading
|
||||
All cards should have skeleton states:
|
||||
```vue
|
||||
<template>
|
||||
<Card v-if="loading">
|
||||
<Skeleton class="aspect-video rounded-t-lg" />
|
||||
<div class="p-4 space-y-2">
|
||||
<Skeleton class="h-5 w-3/4" />
|
||||
<Skeleton class="h-4 w-1/2" />
|
||||
<Skeleton class="h-4 w-1/4" />
|
||||
</div>
|
||||
</Card>
|
||||
<ActualCard v-else :data="data" />
|
||||
</template>
|
||||
```
|
||||
|
||||
### Empty States
|
||||
Provide clear empty states with:
|
||||
- Relevant icon
|
||||
- Clear message
|
||||
- Suggested action (button or link)
|
||||
|
||||
```vue
|
||||
<EmptyState
|
||||
icon="Search"
|
||||
title="No results found"
|
||||
description="Try adjusting your search or filters"
|
||||
>
|
||||
<Button @click="clearFilters">Clear Filters</Button>
|
||||
</EmptyState>
|
||||
```
|
||||
191
.agent/rules/design-system.md
Normal file
191
.agent/rules/design-system.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# ThrillWiki Design System Rules
|
||||
|
||||
Visual identity, colors, typography, and styling guidelines for ThrillWiki UI.
|
||||
|
||||
## Brand Identity
|
||||
|
||||
- **Name**: ThrillWiki (optional "(Beta)" suffix during preview)
|
||||
- **Tagline**: The Ultimate Theme Park Database
|
||||
- **Personality**: Enthusiastic, trustworthy, community-driven
|
||||
- **Visual Style**: Modern, clean, subtle gradients, smooth animations, card-based layouts
|
||||
|
||||
## Color System
|
||||
|
||||
### Semantic Color Tokens (use these, not raw hex values)
|
||||
|
||||
| Token | Purpose | Usage |
|
||||
|-------|---------|-------|
|
||||
| `--background` | Page background | Main content area |
|
||||
| `--foreground` | Primary text | Body text, headings |
|
||||
| `--primary` | Interactive elements | Buttons, links |
|
||||
| `--secondary` | Secondary UI | Secondary buttons |
|
||||
| `--muted` | Subdued content | Hints, disabled states |
|
||||
| `--accent` | Highlights | Focus rings, special callouts |
|
||||
| `--destructive` | Danger actions | Delete buttons, errors |
|
||||
| `--success` | Positive feedback | Success messages, confirmations |
|
||||
| `--warning` | Caution | Alerts, warnings |
|
||||
|
||||
### Status Colors (Entity Badges)
|
||||
- **Operating**: Green (`--success`)
|
||||
- **Closed**: Red (`--destructive`)
|
||||
- **Under Construction**: Amber (`--warning`)
|
||||
|
||||
### Dark Mode
|
||||
- Automatically supported via CSS variables
|
||||
- Reduce contrast (use off-white, not pure white)
|
||||
- Replace shadows with subtle glows
|
||||
- Slightly dim images
|
||||
|
||||
## Typography
|
||||
|
||||
### Font
|
||||
- **Primary Font**: Inter (Sans-Serif)
|
||||
- **Weights**: 400 (Regular), 500 (Medium), 600 (Semibold), 700 (Bold)
|
||||
- **Fallback**: system-ui, sans-serif
|
||||
|
||||
### Type Scale
|
||||
|
||||
| Size | Name | Usage |
|
||||
|------|------|-------|
|
||||
| 48px | Display | Hero headlines |
|
||||
| 36px | H1 | Page titles |
|
||||
| 30px | H2 | Section headers |
|
||||
| 24px | H3 | Card titles |
|
||||
| 20px | H4 | Subheadings |
|
||||
| 16px | Body | Default text |
|
||||
| 14px | Small | Secondary text |
|
||||
| 12px | Caption | Labels, hints |
|
||||
|
||||
### Text Classes (Tailwind)
|
||||
```
|
||||
Page Title: text-4xl font-bold
|
||||
Section Header: text-2xl font-semibold
|
||||
Card Title: text-lg font-medium
|
||||
Body: text-base
|
||||
Caption: text-sm text-muted-foreground
|
||||
```
|
||||
|
||||
## Spacing System
|
||||
|
||||
### Base Unit
|
||||
- 4px base unit
|
||||
- All spacing is multiples of 4
|
||||
|
||||
### Spacing Scale
|
||||
|
||||
| Token | Size | Usage |
|
||||
|-------|------|-------|
|
||||
| `space-1` (p-1) | 4px | Tight gaps |
|
||||
| `space-2` (p-2) | 8px | Icon gaps |
|
||||
| `space-3` (p-3) | 12px | Small padding |
|
||||
| `space-4` (p-4) | 16px | Default padding |
|
||||
| `space-6` (p-6) | 24px | Section gaps |
|
||||
| `space-8` (p-8) | 32px | Large gaps |
|
||||
| `space-12` (p-12) | 48px | Section margins |
|
||||
|
||||
## Border Radius
|
||||
|
||||
| Token | Size | Usage |
|
||||
|-------|------|-------|
|
||||
| `rounded-sm` | 4px | Small elements |
|
||||
| `rounded` | 8px | Buttons, inputs |
|
||||
| `rounded-lg` | 12px | Cards |
|
||||
| `rounded-xl` | 16px | Large cards, modals |
|
||||
| `rounded-full` | 9999px | Pills, avatars |
|
||||
|
||||
## Layout
|
||||
|
||||
### Max Content Width
|
||||
- Main content: `max-w-7xl` (1280px)
|
||||
- Centered with `mx-auto`
|
||||
- Responsive padding: `px-4 md:px-6 lg:px-8`
|
||||
|
||||
### Grid System
|
||||
- Desktop (1024px+): 4 columns
|
||||
- Tablet (768px+): 2 columns
|
||||
- Mobile (<768px): 1 column
|
||||
|
||||
```html
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
```
|
||||
|
||||
## Component Patterns
|
||||
|
||||
### Cards
|
||||
- Background: `bg-card`
|
||||
- Border: `border border-border`
|
||||
- Radius: `rounded-lg`
|
||||
- Padding: `p-4` or `p-6`
|
||||
- Interactive cards: Add `hover:shadow-md transition-shadow cursor-pointer`
|
||||
|
||||
### Buttons
|
||||
| Variant | Classes |
|
||||
|---------|---------|
|
||||
| Primary | `bg-primary text-primary-foreground hover:bg-primary/90` |
|
||||
| Secondary | `bg-secondary text-secondary-foreground hover:bg-secondary/80` |
|
||||
| Outline | `border border-input bg-background hover:bg-accent` |
|
||||
| Ghost | `hover:bg-accent hover:text-accent-foreground` |
|
||||
| Destructive | `bg-destructive text-destructive-foreground hover:bg-destructive/90` |
|
||||
|
||||
### Inputs
|
||||
- Border: `border border-input`
|
||||
- Focus: `focus:ring-2 focus:ring-primary focus:border-transparent`
|
||||
- Error: `border-destructive focus:ring-destructive`
|
||||
|
||||
### Badges
|
||||
```html
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
Operating
|
||||
</span>
|
||||
```
|
||||
|
||||
## Icons
|
||||
|
||||
- **Library**: Lucide icons (via `lucide-vue-next`)
|
||||
- **Default Size**: 24px (w-6 h-6)
|
||||
- **Stroke Width**: 1.5px
|
||||
- **Color**: Inherit from text color
|
||||
|
||||
Common icons:
|
||||
- Search, Menu, User, Heart, Star, MapPin, Calendar, Camera, Edit, Trash, Check, X, ChevronRight, ExternalLink
|
||||
|
||||
## Responsive Breakpoints
|
||||
|
||||
| Breakpoint | Width | Target |
|
||||
|------------|-------|--------|
|
||||
| `sm` | 640px | Large phones |
|
||||
| `md` | 768px | Tablets |
|
||||
| `lg` | 1024px | Small laptops |
|
||||
| `xl` | 1280px | Desktops |
|
||||
| `2xl` | 1536px | Large screens |
|
||||
|
||||
## Accessibility Requirements
|
||||
|
||||
- Color contrast: 4.5:1 minimum for normal text
|
||||
- Focus states: Visible focus ring on all interactive elements
|
||||
- Motion: Respect `prefers-reduced-motion`
|
||||
- Screen readers: Proper ARIA labels on interactive elements
|
||||
- Keyboard: All functionality accessible via keyboard
|
||||
|
||||
## ThrillWiki-Specific Patterns
|
||||
|
||||
### Unit Display
|
||||
Always provide unit conversion toggle (metric/imperial):
|
||||
```vue
|
||||
<UnitDisplay :value="121" type="speed" />
|
||||
<!-- Renders: "121 km/h" or "75 mph" based on user preference -->
|
||||
```
|
||||
|
||||
### Rating Display
|
||||
```vue
|
||||
<RatingDisplay :rating="4.2" :count="156" />
|
||||
<!-- Renders: ★★★★☆ 4.2 (156 reviews) -->
|
||||
```
|
||||
|
||||
### Entity Cards
|
||||
All entity cards (Park, Ride, Company) should show:
|
||||
- Image (with loading skeleton)
|
||||
- Name (primary text)
|
||||
- Key details (secondary text)
|
||||
- Status badge
|
||||
- Quick stats (rating, count, etc.)
|
||||
254
.agent/rules/django-standards.md
Normal file
254
.agent/rules/django-standards.md
Normal file
@@ -0,0 +1,254 @@
|
||||
# ThrillWiki Django Backend Standards
|
||||
|
||||
Rules for developing the ThrillWiki backend with Django and Django REST Framework.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
backend/
|
||||
├── config/ # Project settings
|
||||
│ ├── settings/
|
||||
│ │ ├── base.py # Shared settings
|
||||
│ │ ├── development.py # Dev-specific
|
||||
│ │ └── production.py # Prod-specific
|
||||
│ ├── urls.py # Root URL config
|
||||
│ └── wsgi.py
|
||||
├── apps/
|
||||
│ ├── parks/ # Park-related models and APIs
|
||||
│ ├── rides/ # Ride-related models and APIs
|
||||
│ ├── companies/ # Manufacturers, operators, etc.
|
||||
│ ├── users/ # User profiles, authentication
|
||||
│ ├── reviews/ # User reviews and ratings
|
||||
│ ├── credits/ # Ride credits tracking
|
||||
│ ├── submissions/ # Content submission system
|
||||
│ ├── moderation/ # Moderation queue
|
||||
│ └── core/ # Shared utilities
|
||||
└── tests/ # Test files
|
||||
```
|
||||
|
||||
## Django App Structure
|
||||
|
||||
Each app should follow this structure:
|
||||
```
|
||||
app_name/
|
||||
├── __init__.py
|
||||
├── admin.py # Django admin configuration
|
||||
├── apps.py # App configuration
|
||||
├── models.py # Database models
|
||||
├── serializers.py # DRF serializers
|
||||
├── views.py # DRF viewsets/views
|
||||
├── urls.py # URL routing
|
||||
├── permissions.py # Custom permissions (if needed)
|
||||
├── filters.py # DRF filters (if needed)
|
||||
├── signals.py # Django signals (if needed)
|
||||
└── tests/
|
||||
├── __init__.py
|
||||
├── test_models.py
|
||||
├── test_views.py
|
||||
└── test_serializers.py
|
||||
```
|
||||
|
||||
## Model Conventions
|
||||
|
||||
### Base Model
|
||||
All models should inherit from a base model with common fields:
|
||||
```python
|
||||
from django.db import models
|
||||
import uuid
|
||||
|
||||
class BaseModel(models.Model):
|
||||
"""Base model with common fields"""
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
```
|
||||
|
||||
### Model Example
|
||||
```python
|
||||
class Park(BaseModel):
|
||||
"""A theme park or amusement park"""
|
||||
|
||||
class Status(models.TextChoices):
|
||||
OPERATING = 'operating', 'Operating'
|
||||
CLOSED = 'closed', 'Closed'
|
||||
CONSTRUCTION = 'construction', 'Under Construction'
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
slug = models.SlugField(unique=True)
|
||||
description = models.TextField(blank=True)
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=Status.choices,
|
||||
default=Status.OPERATING
|
||||
)
|
||||
location = models.PointField() # GeoDjango
|
||||
city = models.CharField(max_length=100)
|
||||
country = models.CharField(max_length=100)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
```
|
||||
|
||||
### Versioning for Moderated Content
|
||||
For content that needs version history:
|
||||
```python
|
||||
class ParkVersion(BaseModel):
|
||||
"""Version history for park edits"""
|
||||
park = models.ForeignKey(Park, on_delete=models.CASCADE, related_name='versions')
|
||||
data = models.JSONField() # Snapshot of park data
|
||||
changed_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
|
||||
change_summary = models.CharField(max_length=255)
|
||||
is_current = models.BooleanField(default=False)
|
||||
```
|
||||
|
||||
## API Conventions
|
||||
|
||||
### URL Structure
|
||||
```python
|
||||
# apps/parks/urls.py
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import ParkViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register('parks', ParkViewSet, basename='park')
|
||||
|
||||
urlpatterns = router.urls
|
||||
|
||||
# Results in:
|
||||
# GET /api/parks/ - List parks
|
||||
# POST /api/parks/ - Create park (submission)
|
||||
# GET /api/parks/{slug}/ - Get park detail
|
||||
# PUT /api/parks/{slug}/ - Update park (submission)
|
||||
# DELETE /api/parks/{slug}/ - Delete park (admin only)
|
||||
```
|
||||
|
||||
### ViewSet Structure
|
||||
```python
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import IsAuthenticatedOrReadOnly
|
||||
|
||||
class ParkViewSet(viewsets.ModelViewSet):
|
||||
"""API endpoint for parks"""
|
||||
queryset = Park.objects.all()
|
||||
serializer_class = ParkSerializer
|
||||
permission_classes = [IsAuthenticatedOrReadOnly]
|
||||
lookup_field = 'slug'
|
||||
filterset_class = ParkFilter
|
||||
search_fields = ['name', 'city', 'country']
|
||||
ordering_fields = ['name', 'created_at']
|
||||
|
||||
def get_queryset(self):
|
||||
"""Optimize queries with select_related and prefetch_related"""
|
||||
return Park.objects.select_related(
|
||||
'operator', 'owner'
|
||||
).prefetch_related(
|
||||
'rides', 'photos'
|
||||
)
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def rides(self, request, slug=None):
|
||||
"""Get rides for a specific park"""
|
||||
park = self.get_object()
|
||||
rides = park.rides.all()
|
||||
serializer = RideSerializer(rides, many=True)
|
||||
return Response(serializer.data)
|
||||
```
|
||||
|
||||
### Serializer Patterns
|
||||
```python
|
||||
from rest_framework import serializers
|
||||
|
||||
class ParkSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for Park model"""
|
||||
ride_count = serializers.IntegerField(read_only=True)
|
||||
average_rating = serializers.FloatField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Park
|
||||
fields = [
|
||||
'id', 'name', 'slug', 'description', 'status',
|
||||
'city', 'country', 'ride_count', 'average_rating',
|
||||
'created_at', 'updated_at'
|
||||
]
|
||||
read_only_fields = ['id', 'slug', 'created_at', 'updated_at']
|
||||
|
||||
class ParkDetailSerializer(ParkSerializer):
|
||||
"""Extended serializer for park detail view"""
|
||||
rides = RideSerializer(many=True, read_only=True)
|
||||
photos = PhotoSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta(ParkSerializer.Meta):
|
||||
fields = ParkSerializer.Meta.fields + ['rides', 'photos']
|
||||
```
|
||||
|
||||
## Query Optimization
|
||||
|
||||
- ALWAYS use `select_related()` for ForeignKey relationships
|
||||
- ALWAYS use `prefetch_related()` for ManyToMany and reverse FK relationships
|
||||
- Annotate computed fields at the database level when possible
|
||||
- Use pagination for all list endpoints
|
||||
|
||||
```python
|
||||
# Good
|
||||
parks = Park.objects.select_related('operator').prefetch_related('rides')
|
||||
|
||||
# Bad - causes N+1 queries
|
||||
for park in parks:
|
||||
print(park.operator.name) # Each iteration hits the database
|
||||
```
|
||||
|
||||
## Permissions
|
||||
|
||||
### Custom Permission Classes
|
||||
```python
|
||||
from rest_framework.permissions import BasePermission
|
||||
|
||||
class IsModeratorOrReadOnly(BasePermission):
|
||||
"""Allow read access to all, write access to moderators"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
if request.method in ['GET', 'HEAD', 'OPTIONS']:
|
||||
return True
|
||||
return request.user.is_authenticated and request.user.is_moderator
|
||||
```
|
||||
|
||||
## Submission & Moderation Flow
|
||||
|
||||
All user-submitted content goes through moderation:
|
||||
|
||||
1. User submits content → Creates `Submission` record with status `pending`
|
||||
2. Moderator reviews → Approves or rejects
|
||||
3. On approval → Content is published, version record created
|
||||
|
||||
```python
|
||||
class Submission(BaseModel):
|
||||
class Status(models.TextChoices):
|
||||
PENDING = 'pending', 'Pending Review'
|
||||
APPROVED = 'approved', 'Approved'
|
||||
REJECTED = 'rejected', 'Rejected'
|
||||
CHANGES_REQUESTED = 'changes_requested', 'Changes Requested'
|
||||
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||
object_id = models.UUIDField(null=True, blank=True)
|
||||
data = models.JSONField()
|
||||
status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING)
|
||||
submitted_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
reviewed_by = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
|
||||
review_notes = models.TextField(blank=True)
|
||||
```
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
- Write tests for all models, views, and serializers
|
||||
- Use pytest and pytest-django
|
||||
- Use factories (factory_boy) for test data
|
||||
- Test permissions thoroughly
|
||||
- Test edge cases and error conditions
|
||||
204
.agent/rules/nuxt-standards.md
Normal file
204
.agent/rules/nuxt-standards.md
Normal file
@@ -0,0 +1,204 @@
|
||||
# ThrillWiki Nuxt 4 Frontend Standards
|
||||
|
||||
Rules for developing the ThrillWiki frontend with Nuxt 4.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── app.vue # Root component
|
||||
├── nuxt.config.ts # Nuxt configuration
|
||||
├── pages/ # File-based routing
|
||||
│ ├── index.vue # Homepage (/)
|
||||
│ ├── parks/
|
||||
│ │ ├── index.vue # /parks
|
||||
│ │ ├── nearby.vue # /parks/nearby
|
||||
│ │ └── [slug].vue # /parks/:slug
|
||||
│ └── ...
|
||||
├── components/
|
||||
│ ├── layout/ # Header, Footer, Sidebar
|
||||
│ ├── ui/ # Base components (Button, Card, Input)
|
||||
│ ├── entity/ # ParkCard, RideCard, ReviewCard
|
||||
│ └── forms/ # Form components
|
||||
├── composables/ # Shared logic (useAuth, useApi, useUnits)
|
||||
├── stores/ # Pinia stores
|
||||
├── types/ # TypeScript interfaces
|
||||
└── assets/
|
||||
└── css/ # Global styles, Tailwind config
|
||||
```
|
||||
|
||||
## Component Conventions
|
||||
|
||||
### Naming
|
||||
- Use PascalCase for component files: `ParkCard.vue`, `SearchAutocomplete.vue`
|
||||
- Use kebab-case in templates: `<park-card>`, `<search-autocomplete>`
|
||||
- Prefix base components with `Base`: `BaseButton.vue`, `BaseInput.vue`
|
||||
|
||||
### Component Structure
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// 1. Type imports
|
||||
import type { Park } from '~/types'
|
||||
|
||||
// 2. Component imports (auto-imported usually)
|
||||
|
||||
// 3. Props and emits
|
||||
const props = defineProps<{
|
||||
park: Park
|
||||
variant?: 'default' | 'compact'
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'select', park: Park): void
|
||||
}>()
|
||||
|
||||
// 4. Composables
|
||||
const { formatDistance } = useUnits()
|
||||
|
||||
// 5. Refs and reactive state
|
||||
const isExpanded = ref(false)
|
||||
|
||||
// 6. Computed properties
|
||||
const displayLocation = computed(() =>
|
||||
`${props.park.city}, ${props.park.country}`
|
||||
)
|
||||
|
||||
// 7. Functions
|
||||
function handleClick() {
|
||||
emit('select', props.park)
|
||||
}
|
||||
|
||||
// 8. Lifecycle hooks (if needed)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Template here -->
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Scoped styles, prefer Tailwind classes in template */
|
||||
</style>
|
||||
```
|
||||
|
||||
### TypeScript
|
||||
- Enable strict mode
|
||||
- Define interfaces for all data structures in `types/`
|
||||
- Use `defineProps<T>()` with TypeScript generics
|
||||
- No `any` types without explicit justification
|
||||
|
||||
## Routing
|
||||
|
||||
### File-Based Routes
|
||||
Follow Nuxt 4 file-based routing conventions:
|
||||
- `pages/index.vue` → `/`
|
||||
- `pages/parks/index.vue` → `/parks`
|
||||
- `pages/parks/[slug].vue` → `/parks/:slug`
|
||||
- `pages/parks/[park]/rides/[ride].vue` → `/parks/:park/rides/:ride`
|
||||
|
||||
### Navigation
|
||||
```typescript
|
||||
// Use navigateTo for programmatic navigation
|
||||
await navigateTo('/parks/cedar-point')
|
||||
|
||||
// Use NuxtLink for declarative navigation
|
||||
<NuxtLink to="/parks">All Parks</NuxtLink>
|
||||
```
|
||||
|
||||
## Data Fetching
|
||||
|
||||
### Use Composables
|
||||
```typescript
|
||||
// composables/useParks.ts
|
||||
export function useParks() {
|
||||
const { data, pending, error, refresh } = useFetch('/api/parks/')
|
||||
|
||||
return {
|
||||
parks: data,
|
||||
loading: pending,
|
||||
error,
|
||||
refresh
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### In Components
|
||||
```typescript
|
||||
// Use useAsyncData for page-level data
|
||||
const { data: park } = await useAsyncData(
|
||||
`park-${route.params.slug}`,
|
||||
() => $fetch(`/api/parks/${route.params.slug}/`)
|
||||
)
|
||||
```
|
||||
|
||||
## State Management (Pinia)
|
||||
|
||||
### Store Structure
|
||||
```typescript
|
||||
// stores/auth.ts
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const user = ref<User | null>(null)
|
||||
const isAuthenticated = computed(() => !!user.value)
|
||||
|
||||
async function login(credentials: LoginCredentials) {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
function logout() {
|
||||
user.value = null
|
||||
}
|
||||
|
||||
return { user, isAuthenticated, login, logout }
|
||||
})
|
||||
```
|
||||
|
||||
### Using Stores
|
||||
```typescript
|
||||
const authStore = useAuthStore()
|
||||
const { user, isAuthenticated } = storeToRefs(authStore)
|
||||
```
|
||||
|
||||
## API Integration
|
||||
|
||||
### Base API Composable
|
||||
```typescript
|
||||
// composables/useApi.ts
|
||||
export function useApi() {
|
||||
const config = useRuntimeConfig()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
return $fetch.create({
|
||||
baseURL: config.public.apiBase,
|
||||
headers: {
|
||||
...(authStore.token && { Authorization: `Bearer ${authStore.token}` })
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Page Errors
|
||||
```typescript
|
||||
// In page components
|
||||
const { data, error } = await useAsyncData(...)
|
||||
|
||||
if (error.value) {
|
||||
throw createError({
|
||||
statusCode: error.value.statusCode || 500,
|
||||
message: error.value.message
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Form Errors
|
||||
- Display validation errors inline with form fields
|
||||
- Use toast notifications for API errors
|
||||
- Provide clear user feedback
|
||||
|
||||
## Accessibility Requirements
|
||||
|
||||
- All interactive elements must be keyboard accessible
|
||||
- Provide proper ARIA labels
|
||||
- Ensure color contrast meets WCAG AA standards
|
||||
- Support `prefers-reduced-motion`
|
||||
- Use semantic HTML elements
|
||||
85
.agent/workflows/comply.md
Normal file
85
.agent/workflows/comply.md
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
description: Ensure compliance with source_docs specifications - Continuous guard
|
||||
---
|
||||
|
||||
# Source Docs Compliance Workflow
|
||||
|
||||
You are now in **Compliance Guard Mode**. The documents in `source_docs/` are LAW. Every code change must comply with these specifications.
|
||||
|
||||
## The Constitution
|
||||
|
||||
The following documents are the single source of truth:
|
||||
- `source_docs/SITE_OVERVIEW.md` - High-level product vision
|
||||
- `source_docs/DESIGN_SYSTEM.md` - Visual identity, colors, typography, gradients
|
||||
- `source_docs/COMPONENTS.md` - Component specifications and patterns
|
||||
- `source_docs/PAGES.md` - Page layouts and content
|
||||
- `source_docs/USER_FLOWS.md` - User journeys and interaction specifications
|
||||
|
||||
## Before Making ANY Code Change
|
||||
|
||||
1. **Identify Relevant Specs**: Determine which source_docs apply to the change
|
||||
2. **Read the Spec**: View the relevant sections to understand requirements
|
||||
3. **Check for Deviations**: Compare current/proposed code against the spec
|
||||
4. **Cite Your Sources**: Reference specific line numbers when claiming compliance
|
||||
|
||||
## Compliance Checklist
|
||||
|
||||
### For Components (check against COMPONENTS.md, DESIGN_SYSTEM.md):
|
||||
- [ ] Uses correct component structure (header, content, footer)
|
||||
- [ ] Uses primary color palette (blue for primary, NOT emerald/teal unless specified)
|
||||
- [ ] Uses centralized constants instead of hardcoded options
|
||||
- [ ] Follows established patterns (sticky footer, tab navigation, etc.)
|
||||
- [ ] Proper button variants per spec
|
||||
- [ ] Dark mode support
|
||||
|
||||
### For User Flows (check against USER_FLOWS.md):
|
||||
- [ ] All required fields present (e.g., submission notes for edits)
|
||||
- [ ] Proper validation implemented
|
||||
- [ ] Error states handled per spec
|
||||
- [ ] Success feedback matches spec
|
||||
|
||||
### For Forms:
|
||||
- [ ] Options imported from `~/utils/constants.ts`
|
||||
- [ ] Backend-synced choices (check `backend/apps/*/choices.py`)
|
||||
- [ ] Required fields marked and validated
|
||||
- [ ] Proper help text and hints
|
||||
|
||||
## When Deviation is Found
|
||||
|
||||
1. **STOP** - Do not proceed with the current approach
|
||||
2. **LOG** - Document the deviation with:
|
||||
- Requirement (from source_docs)
|
||||
- Current implementation
|
||||
- Proposed fix
|
||||
3. **FIX** - Implement the compliant solution
|
||||
4. **VERIFY** - Ensure the fix matches the spec
|
||||
|
||||
## Status Tags
|
||||
|
||||
Use these in any audit or review:
|
||||
- `[OK]` - Compliant with spec
|
||||
- `[DEVIATION]` - Implemented differently than spec
|
||||
- `[MISSING]` - Required feature not implemented
|
||||
- `[RISK]` - Potential issue that needs investigation
|
||||
|
||||
## Key Constants Files
|
||||
|
||||
Always use these instead of hardcoding:
|
||||
- `frontend/app/utils/constants.ts` - Status configs, park types, etc.
|
||||
- `backend/apps/parks/choices.py` - Park status and type definitions
|
||||
- `backend/apps/rides/choices.py` - Ride status and category definitions
|
||||
- `backend/apps/moderation/choices.py` - Submission status definitions
|
||||
|
||||
## Example Audit Output
|
||||
|
||||
```markdown
|
||||
## Compliance Audit: ComponentName.vue
|
||||
|
||||
### Checking Against: [List relevant docs]
|
||||
|
||||
| Requirement | Source | Status |
|
||||
|-------------|--------|--------|
|
||||
| Uses primary gradient | DESIGN_SYSTEM.md L91-98 | [OK] |
|
||||
| Submission note field | USER_FLOWS.md L473-476 | [MISSING] → FIX |
|
||||
| Sticky footer pattern | COMPONENTS.md L583-602 | [DEVIATION] → FIX |
|
||||
```
|
||||
168
.agent/workflows/migrate-component.md
Normal file
168
.agent/workflows/migrate-component.md
Normal file
@@ -0,0 +1,168 @@
|
||||
---
|
||||
description: Migrate a React component from thrillwiki-87 to Vue/Nuxt
|
||||
---
|
||||
|
||||
# Migrate Component Workflow
|
||||
|
||||
Convert a React component from thrillwiki-87 (the authoritative source) to Vue 3 Composition API for the Nuxt 4 project.
|
||||
|
||||
## Step 1: Locate Source Component
|
||||
|
||||
Find the React component in thrillwiki-87:
|
||||
```bash
|
||||
# React components are in:
|
||||
/Volumes/macminissd/Projects/thrillwiki-87/src/components/
|
||||
```
|
||||
|
||||
Common component directories:
|
||||
- `auth/` - Authentication components
|
||||
- `parks/` - Park-related components
|
||||
- `rides/` - Ride-related components
|
||||
- `forms/` - Form components
|
||||
- `moderation/` - Moderation queue components
|
||||
- `ui/` - Base UI primitives (shadcn-ui)
|
||||
- `common/` - Shared utilities
|
||||
- `layout/` - Layout components
|
||||
|
||||
## Step 2: Analyze React Component
|
||||
|
||||
Extract these patterns from the source:
|
||||
|
||||
```tsx
|
||||
// Props interface
|
||||
interface ComponentProps {
|
||||
prop1: string
|
||||
prop2?: number
|
||||
}
|
||||
|
||||
// State hooks
|
||||
const [value, setValue] = useState<Type>(initial)
|
||||
|
||||
// Effects
|
||||
useEffect(() => { /* logic */ }, [deps])
|
||||
|
||||
// Event handlers
|
||||
const handleClick = () => { /* logic */ }
|
||||
|
||||
// Render return
|
||||
return <JSX />
|
||||
```
|
||||
|
||||
## Step 3: Translate to Vue 3
|
||||
|
||||
### Props
|
||||
```tsx
|
||||
// React
|
||||
interface Props { name: string; count?: number }
|
||||
```
|
||||
↓
|
||||
```vue
|
||||
<!-- Vue -->
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
name: string
|
||||
count?: number
|
||||
}>()
|
||||
</script>
|
||||
```
|
||||
|
||||
### State
|
||||
```tsx
|
||||
// React
|
||||
const [value, setValue] = useState<string>('')
|
||||
```
|
||||
↓
|
||||
```vue
|
||||
<!-- Vue -->
|
||||
<script setup lang="ts">
|
||||
const value = ref<string>('')
|
||||
</script>
|
||||
```
|
||||
|
||||
### Effects
|
||||
```tsx
|
||||
// React
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
}, [id])
|
||||
```
|
||||
↓
|
||||
```vue
|
||||
<!-- Vue -->
|
||||
<script setup lang="ts">
|
||||
watch(() => id, () => {
|
||||
fetchData()
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
```
|
||||
|
||||
### Events
|
||||
```tsx
|
||||
// React
|
||||
onClick={() => onSelect(item)}
|
||||
```
|
||||
↓
|
||||
```vue
|
||||
<!-- Vue -->
|
||||
@click="emit('select', item)"
|
||||
```
|
||||
|
||||
## Step 4: Map UI Components
|
||||
|
||||
Translate shadcn-ui to Nuxt UI:
|
||||
|
||||
| shadcn-ui | Nuxt UI |
|
||||
|-----------|---------|
|
||||
| `<Button>` | `<UButton>` |
|
||||
| `<Card>` | `<UCard>` |
|
||||
| `<Dialog>` | `<UModal>` |
|
||||
| `<Input>` | `<UInput>` |
|
||||
| `<Select>` | `<USelect>` |
|
||||
| `<Tabs>` | `<UTabs>` |
|
||||
| `<Badge>` | `<UBadge>` |
|
||||
|
||||
## Step 5: Update API Calls
|
||||
|
||||
Replace Supabase with Django API:
|
||||
|
||||
```tsx
|
||||
// React (Supabase)
|
||||
const { data } = await supabase.from('parks').select('*')
|
||||
```
|
||||
↓
|
||||
```vue
|
||||
<!-- Vue (Django) -->
|
||||
<script setup lang="ts">
|
||||
const { data } = await useApi<Park[]>('/parks/')
|
||||
</script>
|
||||
```
|
||||
|
||||
## Step 6: Place in Nuxt Structure
|
||||
|
||||
Target paths:
|
||||
```
|
||||
frontend/app/components/
|
||||
├── auth/ # Auth components
|
||||
├── cards/ # Card components
|
||||
├── common/ # Shared utilities
|
||||
├── modals/ # Modal dialogs
|
||||
├── rides/ # Ride components
|
||||
└── ui/ # Base UI components
|
||||
```
|
||||
|
||||
## Step 7: Verify Parity
|
||||
|
||||
- [ ] All props from source are present
|
||||
- [ ] All state variables ported
|
||||
- [ ] All event handlers work
|
||||
- [ ] Styling matches (Tailwind classes)
|
||||
- [ ] Loading states present
|
||||
- [ ] Error states handled
|
||||
- [ ] Dark mode works
|
||||
|
||||
## Example Migration
|
||||
|
||||
**Source**: `thrillwiki-87/src/components/parks/ParkCard.tsx`
|
||||
**Target**: `frontend/app/components/cards/ParkCard.vue`
|
||||
|
||||
Check existing target. If it exists, compare and add missing features. If not, create new.
|
||||
223
.agent/workflows/migrate-hook.md
Normal file
223
.agent/workflows/migrate-hook.md
Normal file
@@ -0,0 +1,223 @@
|
||||
---
|
||||
description: Convert a React hook from thrillwiki-87 to a Vue composable
|
||||
---
|
||||
|
||||
# Migrate Hook Workflow
|
||||
|
||||
Convert a React hook from thrillwiki-87 to a Vue 3 composable for the Nuxt 4 project.
|
||||
|
||||
## Step 1: Locate Source Hook
|
||||
|
||||
Find the React hook in thrillwiki-87:
|
||||
```bash
|
||||
/Volumes/macminissd/Projects/thrillwiki-87/src/hooks/
|
||||
```
|
||||
|
||||
Key hooks (80+ total):
|
||||
- `useAuth.tsx` - Authentication state
|
||||
- `useModerationQueue.ts` - Moderation logic (21KB)
|
||||
- `useEntityVersions.ts` - Version history (14KB)
|
||||
- `useSearch.tsx` - Search functionality
|
||||
- `useUnitPreferences.ts` - Unit conversion
|
||||
- `useProfile.tsx` - User profile
|
||||
- `useLocations.ts` - Location data
|
||||
- `useRideCreditFilters.ts` - Credit filtering
|
||||
|
||||
## Step 2: Analyze Hook Pattern
|
||||
|
||||
Extract the hook structure:
|
||||
|
||||
```tsx
|
||||
export function useFeature(params: Params) {
|
||||
// State
|
||||
const [data, setData] = useState<Type>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
// Effects
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
}, [dependency])
|
||||
|
||||
// Actions
|
||||
const doSomething = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await api.call()
|
||||
setData(result)
|
||||
} catch (e) {
|
||||
setError(e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return { data, loading, error, doSomething }
|
||||
}
|
||||
```
|
||||
|
||||
## Step 3: Convert to Composable
|
||||
|
||||
### Basic Structure
|
||||
```tsx
|
||||
// React
|
||||
export function useFeature() {
|
||||
const [value, setValue] = useState('')
|
||||
return { value, setValue }
|
||||
}
|
||||
```
|
||||
↓
|
||||
```typescript
|
||||
// Vue
|
||||
export function useFeature() {
|
||||
const value = ref('')
|
||||
|
||||
function setValue(newValue: string) {
|
||||
value.value = newValue
|
||||
}
|
||||
|
||||
return { value, setValue }
|
||||
}
|
||||
```
|
||||
|
||||
### State Conversions
|
||||
```tsx
|
||||
// React
|
||||
const [count, setCount] = useState(0)
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [items, setItems] = useState<Item[]>([])
|
||||
```
|
||||
↓
|
||||
```typescript
|
||||
// Vue
|
||||
const count = ref(0)
|
||||
const user = ref<User | null>(null)
|
||||
const items = ref<Item[]>([])
|
||||
```
|
||||
|
||||
### Effect Conversions
|
||||
```tsx
|
||||
// React - Run on mount
|
||||
useEffect(() => {
|
||||
initialize()
|
||||
}, [])
|
||||
```
|
||||
↓
|
||||
```typescript
|
||||
// Vue
|
||||
onMounted(() => {
|
||||
initialize()
|
||||
})
|
||||
```
|
||||
|
||||
```tsx
|
||||
// React - Watch dependency
|
||||
useEffect(() => {
|
||||
fetchData(id)
|
||||
}, [id])
|
||||
```
|
||||
↓
|
||||
```typescript
|
||||
// Vue
|
||||
watch(() => id, (newId) => {
|
||||
fetchData(newId)
|
||||
}, { immediate: true })
|
||||
```
|
||||
|
||||
### Supabase → Django API
|
||||
```tsx
|
||||
// React (Supabase)
|
||||
const { data } = await supabase
|
||||
.from('parks')
|
||||
.select('*')
|
||||
.eq('slug', slug)
|
||||
.single()
|
||||
```
|
||||
↓
|
||||
```typescript
|
||||
// Vue (Django)
|
||||
const api = useApi()
|
||||
const { data } = await api<Park>(`/parks/${slug}/`)
|
||||
```
|
||||
|
||||
## Step 4: Handle Complex Patterns
|
||||
|
||||
### useCallback → Plain Function
|
||||
```tsx
|
||||
// React
|
||||
const memoizedFn = useCallback(() => {
|
||||
doSomething(dep)
|
||||
}, [dep])
|
||||
```
|
||||
↓
|
||||
```typescript
|
||||
// Vue - Usually no memo needed
|
||||
function doSomething() {
|
||||
// Vue's reactivity handles this
|
||||
}
|
||||
```
|
||||
|
||||
### useMemo → computed
|
||||
```tsx
|
||||
// React
|
||||
const derived = useMemo(() => expensiveCalc(data), [data])
|
||||
```
|
||||
↓
|
||||
```typescript
|
||||
// Vue
|
||||
const derived = computed(() => expensiveCalc(data.value))
|
||||
```
|
||||
|
||||
### Custom Hook Composition
|
||||
```tsx
|
||||
// React
|
||||
function useFeature() {
|
||||
const auth = useAuth()
|
||||
const { data } = useQuery(...)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
↓
|
||||
```typescript
|
||||
// Vue
|
||||
export function useFeature() {
|
||||
const { user } = useAuth()
|
||||
const api = useApi()
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Step 5: Target Location
|
||||
|
||||
Place composables in:
|
||||
```
|
||||
frontend/app/composables/
|
||||
├── useApi.ts # Base API client
|
||||
├── useAuth.ts # Authentication
|
||||
├── useParksApi.ts # Parks API
|
||||
├── useRidesApi.ts # Rides API
|
||||
├── useModeration.ts # Moderation queue
|
||||
└── use[Feature].ts # New composables
|
||||
```
|
||||
|
||||
## Step 6: Verify Parity
|
||||
|
||||
- [ ] All returned values present
|
||||
- [ ] All actions/methods work
|
||||
- [ ] State updates correctly
|
||||
- [ ] API calls translated
|
||||
- [ ] Error handling maintained
|
||||
- [ ] Loading states work
|
||||
- [ ] TypeScript types correct
|
||||
|
||||
## Priority Hooks to Migrate
|
||||
|
||||
| Hook | Size | Complexity |
|
||||
|------|------|------------|
|
||||
| useModerationQueue.ts | 21KB | High |
|
||||
| useEntityVersions.ts | 14KB | High |
|
||||
| useAuth.tsx | 11KB | Medium |
|
||||
| useAutoComplete.ts | 10KB | Medium |
|
||||
| useRateLimitAlerts.ts | 10KB | Medium |
|
||||
| useRideCreditFilters.ts | 9KB | Medium |
|
||||
| useAdminSettings.ts | 9KB | Medium |
|
||||
183
.agent/workflows/migrate-page.md
Normal file
183
.agent/workflows/migrate-page.md
Normal file
@@ -0,0 +1,183 @@
|
||||
---
|
||||
description: Migrate a React page from thrillwiki-87 to Nuxt 4
|
||||
---
|
||||
|
||||
# Migrate Page Workflow
|
||||
|
||||
Port a React page from thrillwiki-87 (the authoritative source) to a Nuxt 4 page.
|
||||
|
||||
## Step 1: Locate Source Page
|
||||
|
||||
Find the React page in thrillwiki-87:
|
||||
```bash
|
||||
/Volumes/macminissd/Projects/thrillwiki-87/src/pages/
|
||||
```
|
||||
|
||||
Key pages:
|
||||
- `Index.tsx` - Homepage
|
||||
- `Parks.tsx` - Parks listing
|
||||
- `ParkDetail.tsx` - Individual park (36KB - complex)
|
||||
- `Rides.tsx` - Rides listing
|
||||
- `RideDetail.tsx` - Individual ride (54KB - most complex)
|
||||
- `Profile.tsx` - User profile (51KB - complex)
|
||||
- `Search.tsx` - Global search
|
||||
- `AdminDashboard.tsx` - Admin panel
|
||||
|
||||
## Step 2: Analyze Page Structure
|
||||
|
||||
Extract from React page:
|
||||
|
||||
```tsx
|
||||
// Route params
|
||||
const { id } = useParams()
|
||||
|
||||
// Data fetching
|
||||
const { data, isLoading, error } = useQuery(...)
|
||||
|
||||
// SEO
|
||||
// (usually react-helmet or similar)
|
||||
|
||||
// Page layout structure
|
||||
return (
|
||||
<Layout>
|
||||
<Tabs>...</Tabs>
|
||||
<Content>...</Content>
|
||||
</Layout>
|
||||
)
|
||||
```
|
||||
|
||||
## Step 3: Create Nuxt Page
|
||||
|
||||
### Route Params
|
||||
```tsx
|
||||
// React
|
||||
const { parkSlug } = useParams()
|
||||
```
|
||||
↓
|
||||
```vue
|
||||
<!-- Nuxt -->
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
const parkSlug = route.params.park_slug as string
|
||||
</script>
|
||||
```
|
||||
|
||||
### Data Fetching
|
||||
```tsx
|
||||
// React (React Query)
|
||||
const { data, isLoading } = useQuery(['park', id], () => fetchPark(id))
|
||||
```
|
||||
↓
|
||||
```vue
|
||||
<!-- Nuxt -->
|
||||
<script setup lang="ts">
|
||||
const { data, pending } = await useAsyncData(
|
||||
`park-${parkSlug}`,
|
||||
() => useParksApi().getPark(parkSlug)
|
||||
)
|
||||
</script>
|
||||
```
|
||||
|
||||
### SEO Meta
|
||||
```tsx
|
||||
// React (Helmet)
|
||||
<Helmet>
|
||||
<title>{park.name} | ThrillWiki</title>
|
||||
</Helmet>
|
||||
```
|
||||
↓
|
||||
```vue
|
||||
<!-- Nuxt -->
|
||||
<script setup lang="ts">
|
||||
useSeoMeta({
|
||||
title: () => `${data.value?.name} | ThrillWiki`,
|
||||
description: () => data.value?.description
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### Page Meta
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
middleware: ['auth'], // if authentication required
|
||||
layout: 'default'
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
## Step 4: Port Page Sections
|
||||
|
||||
Common patterns:
|
||||
|
||||
### Tabs Structure
|
||||
```tsx
|
||||
// React
|
||||
<Tabs defaultValue="overview">
|
||||
<TabsList>
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="overview">...</TabsContent>
|
||||
</Tabs>
|
||||
```
|
||||
↓
|
||||
```vue
|
||||
<!-- Nuxt -->
|
||||
<UTabs :items="tabs" v-model="activeTab">
|
||||
<template #default="{ item }">
|
||||
<component :is="item.component" v-bind="item.props" />
|
||||
</template>
|
||||
</UTabs>
|
||||
```
|
||||
|
||||
### Loading States
|
||||
```tsx
|
||||
// React
|
||||
{isLoading ? <Skeleton /> : <Content />}
|
||||
```
|
||||
↓
|
||||
```vue
|
||||
<!-- Nuxt -->
|
||||
<template>
|
||||
<div v-if="pending">
|
||||
<USkeleton class="h-48" />
|
||||
</div>
|
||||
<div v-else-if="data">
|
||||
<!-- Content -->
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Step 5: Target Location
|
||||
|
||||
Nuxt pages use file-based routing:
|
||||
|
||||
| React Route | Nuxt File Path |
|
||||
|-------------|----------------|
|
||||
| `/parks` | `pages/parks/index.vue` |
|
||||
| `/parks/:slug` | `pages/parks/[park_slug]/index.vue` |
|
||||
| `/parks/:slug/rides/:ride` | `pages/parks/[park_slug]/rides/[ride_slug].vue` |
|
||||
| `/manufacturers/:id` | `pages/manufacturers/[slug].vue` |
|
||||
|
||||
## Step 6: Verify Feature Parity
|
||||
|
||||
Compare source page with target:
|
||||
- [ ] All tabs/sections present
|
||||
- [ ] All data displayed
|
||||
- [ ] All actions work (edit, delete, etc.)
|
||||
- [ ] Responsive layout matches
|
||||
- [ ] Loading states present
|
||||
- [ ] Error handling works
|
||||
- [ ] SEO meta correct
|
||||
|
||||
## Reference: Page Complexity
|
||||
|
||||
| Page | Source Size | Priority |
|
||||
|------|-------------|----------|
|
||||
| RideDetail.tsx | 54KB | High |
|
||||
| Profile.tsx | 51KB | High |
|
||||
| AdminSettings.tsx | 44KB | Medium |
|
||||
| ParkDetail.tsx | 36KB | High |
|
||||
| Auth.tsx | 29KB | Medium |
|
||||
| Parks.tsx | 22KB | High |
|
||||
| Rides.tsx | 20KB | High |
|
||||
201
.agent/workflows/migrate-type.md
Normal file
201
.agent/workflows/migrate-type.md
Normal file
@@ -0,0 +1,201 @@
|
||||
---
|
||||
description: Port TypeScript types from thrillwiki-87 to the Nuxt project
|
||||
---
|
||||
|
||||
# Migrate Type Workflow
|
||||
|
||||
Port TypeScript type definitions from thrillwiki-87 to the Nuxt 4 project, ensuring sync with Django serializers.
|
||||
|
||||
## Step 1: Locate Source Types
|
||||
|
||||
Find types in thrillwiki-87:
|
||||
```bash
|
||||
/Volumes/macminissd/Projects/thrillwiki-87/src/types/
|
||||
```
|
||||
|
||||
Key type files (63 total):
|
||||
- `database.ts` - Core entity types (12KB)
|
||||
- `statuses.ts` - Status enums (13KB)
|
||||
- `moderation.ts` - Moderation types (10KB)
|
||||
- `rideAttributes.ts` - Ride specs (8KB)
|
||||
- `versioning.ts` - Version history (7KB)
|
||||
- `submissions.ts` - Submission types
|
||||
- `company.ts` - Company entities
|
||||
|
||||
## Step 2: Analyze Source Types
|
||||
|
||||
Extract type structure:
|
||||
|
||||
```typescript
|
||||
// thrillwiki-87/src/types/database.ts
|
||||
export interface Park {
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
park_type: ParkType
|
||||
status: ParkStatus
|
||||
opening_date?: string
|
||||
closing_date?: string
|
||||
location?: ParkLocation
|
||||
// ...
|
||||
}
|
||||
|
||||
export interface ParkLocation {
|
||||
latitude: number
|
||||
longitude: number
|
||||
country: string
|
||||
city?: string
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Step 3: Check Django Serializers
|
||||
|
||||
Before porting, verify Django backend has matching fields:
|
||||
|
||||
```bash
|
||||
# Check serializers in:
|
||||
backend/apps/parks/serializers.py
|
||||
backend/apps/rides/serializers.py
|
||||
backend/apps/companies/serializers.py
|
||||
```
|
||||
|
||||
The Django serializer defines what the API returns:
|
||||
```python
|
||||
class ParkSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Park
|
||||
fields = ['id', 'name', 'slug', 'park_type', 'status', ...]
|
||||
```
|
||||
|
||||
## Step 4: Handle Naming Conventions
|
||||
|
||||
Django uses snake_case, TypeScript typically uses camelCase. Choose one strategy:
|
||||
|
||||
### Option A: Match Django exactly (Recommended)
|
||||
```typescript
|
||||
// Keep snake_case from API
|
||||
export interface Park {
|
||||
id: string
|
||||
name: string
|
||||
park_type: string
|
||||
created_at: string
|
||||
}
|
||||
```
|
||||
|
||||
### Option B: Transform in API layer
|
||||
```typescript
|
||||
// camelCase in types
|
||||
export interface Park {
|
||||
id: string
|
||||
name: string
|
||||
parkType: string
|
||||
createdAt: string
|
||||
}
|
||||
// Transform in useApi or serializer
|
||||
```
|
||||
|
||||
**Current project uses Option A (snake_case).**
|
||||
|
||||
## Step 5: Port the Types
|
||||
|
||||
Create/update type files:
|
||||
|
||||
```typescript
|
||||
// frontend/app/types/park.ts
|
||||
export interface Park {
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
park_type: string
|
||||
status: string
|
||||
short_description?: string
|
||||
description?: string
|
||||
opening_date?: string
|
||||
closing_date?: string
|
||||
operating_season?: string
|
||||
website?: string
|
||||
latitude?: number
|
||||
longitude?: number
|
||||
// Match Django serializer fields exactly
|
||||
}
|
||||
|
||||
export interface ParkDetail extends Park {
|
||||
rides?: Ride[]
|
||||
reviews?: Review[]
|
||||
photos?: Photo[]
|
||||
// Extended fields from detail serializer
|
||||
}
|
||||
```
|
||||
|
||||
## Step 6: Port Enums/Options
|
||||
|
||||
Source may have enums that should be constants:
|
||||
|
||||
```typescript
|
||||
// thrillwiki-87/src/types/statuses.ts
|
||||
export const PARK_STATUS = {
|
||||
OPERATING: 'operating',
|
||||
CLOSED: 'closed',
|
||||
// ...
|
||||
} as const
|
||||
```
|
||||
↓
|
||||
```typescript
|
||||
// frontend/app/utils/constants.ts
|
||||
export const PARK_STATUS_OPTIONS = [
|
||||
{ value: 'operating', label: 'Operating', color: 'green' },
|
||||
{ value: 'closed', label: 'Closed', color: 'red' },
|
||||
// Sync with backend/apps/parks/choices.py
|
||||
]
|
||||
```
|
||||
|
||||
## Step 7: Target Locations
|
||||
|
||||
```
|
||||
frontend/app/types/
|
||||
├── index.ts # Re-exports and common types
|
||||
├── park.ts # Park types
|
||||
├── ride.ts # Ride types
|
||||
├── company.ts # Company types (manufacturer, designer, etc.)
|
||||
├── user.ts # User/profile types
|
||||
├── moderation.ts # Moderation types
|
||||
└── api.ts # API response wrappers
|
||||
```
|
||||
|
||||
## Step 8: Verify Type Sync
|
||||
|
||||
| Layer | File | Must Match |
|
||||
|-------|------|------------|
|
||||
| Django Model | `backend/apps/*/models.py` | Database schema |
|
||||
| Serializer | `backend/apps/*/serializers.py` | API response |
|
||||
| Frontend Type | `frontend/app/types/*.ts` | Serializer fields |
|
||||
|
||||
Run checks:
|
||||
```bash
|
||||
# Check Django fields
|
||||
python manage.py shell -c "from apps.parks.models import Park; print([f.name for f in Park._meta.fields])"
|
||||
|
||||
# Check serializer fields
|
||||
python manage.py shell -c "from apps.parks.serializers import ParkSerializer; print(ParkSerializer().fields.keys())"
|
||||
```
|
||||
|
||||
## Step 9: Checklist
|
||||
|
||||
- [ ] All source type fields ported
|
||||
- [ ] Fields match Django serializer
|
||||
- [ ] snake_case naming used
|
||||
- [ ] Optional fields marked with `?`
|
||||
- [ ] Enums/options in constants.ts
|
||||
- [ ] Backend choices.py synced
|
||||
- [ ] Types exported from index.ts
|
||||
|
||||
## Priority Types
|
||||
|
||||
| Type File | Source Size | Notes |
|
||||
|-----------|-------------|-------|
|
||||
| statuses.ts | 13KB | All status enums |
|
||||
| database.ts | 12KB | Core entities |
|
||||
| moderation.ts | 10KB | Queue types |
|
||||
| rideAttributes.ts | 8KB | Spec options |
|
||||
| versioning.ts | 7KB | History types |
|
||||
193
.agent/workflows/migrate.md
Normal file
193
.agent/workflows/migrate.md
Normal file
@@ -0,0 +1,193 @@
|
||||
---
|
||||
description: Audit gaps and implement missing features from thrillwiki-87 source
|
||||
---
|
||||
|
||||
# Migration Workflow
|
||||
|
||||
**thrillwiki-87 is LAW.** This workflow audits what's missing and implements it.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Run `/migrate` to:
|
||||
1. Audit current project against thrillwiki-87
|
||||
2. Identify the highest-priority missing feature
|
||||
3. Implement it using the appropriate sub-workflow
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Gap Analysis
|
||||
|
||||
### Step 1.1: Audit Components
|
||||
|
||||
Compare React components with Vue components:
|
||||
|
||||
```bash
|
||||
# Source (LAW):
|
||||
/Volumes/macminissd/Projects/thrillwiki-87/src/components/
|
||||
|
||||
# Target:
|
||||
/Volumes/macminissd/Projects/thrillwiki_django_no_react/frontend/app/components/
|
||||
```
|
||||
|
||||
For each React component directory, check if equivalent Vue component exists:
|
||||
- **EXISTS**: Mark as ✅, check for feature parity
|
||||
- **MISSING**: Mark as ❌, add to implementation queue
|
||||
|
||||
### Step 1.2: Audit Pages
|
||||
|
||||
Compare React pages with Nuxt pages:
|
||||
|
||||
```bash
|
||||
# Source (LAW):
|
||||
/Volumes/macminissd/Projects/thrillwiki-87/src/pages/
|
||||
|
||||
# Target:
|
||||
/Volumes/macminissd/Projects/thrillwiki_django_no_react/frontend/app/pages/
|
||||
```
|
||||
|
||||
Key pages to audit (by size/complexity):
|
||||
| React Page | Size | Priority |
|
||||
|------------|------|----------|
|
||||
| RideDetail.tsx | 54KB | P0 |
|
||||
| Profile.tsx | 51KB | P0 |
|
||||
| AdminSettings.tsx | 44KB | P1 |
|
||||
| ParkDetail.tsx | 36KB | P0 |
|
||||
| Auth.tsx | 29KB | P1 |
|
||||
| Parks.tsx | 22KB | P0 |
|
||||
| Rides.tsx | 20KB | P0 |
|
||||
|
||||
### Step 1.3: Audit Hooks → Composables
|
||||
|
||||
Compare React hooks with Vue composables:
|
||||
|
||||
```bash
|
||||
# Source (LAW):
|
||||
/Volumes/macminissd/Projects/thrillwiki-87/src/hooks/
|
||||
|
||||
# Target:
|
||||
/Volumes/macminissd/Projects/thrillwiki_django_no_react/frontend/app/composables/
|
||||
```
|
||||
|
||||
Priority hooks:
|
||||
| React Hook | Size | Current Status |
|
||||
|------------|------|----------------|
|
||||
| useModerationQueue.ts | 21KB | Check useModeration.ts |
|
||||
| useEntityVersions.ts | 14KB | ❌ Missing |
|
||||
| useAuth.tsx | 11KB | Check useAuth.ts |
|
||||
| useRideCreditFilters.ts | 9KB | ❌ Missing |
|
||||
|
||||
### Step 1.4: Audit Types
|
||||
|
||||
Compare TypeScript definitions:
|
||||
|
||||
```bash
|
||||
# Source (LAW):
|
||||
/Volumes/macminissd/Projects/thrillwiki-87/src/types/
|
||||
|
||||
# Target:
|
||||
/Volumes/macminissd/Projects/thrillwiki_django_no_react/frontend/app/types/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Priority Selection
|
||||
|
||||
After auditing, select the highest priority gap:
|
||||
|
||||
### Priority Matrix
|
||||
|
||||
| Category | Weight | Examples |
|
||||
|----------|--------|----------|
|
||||
| **P0 - Core UX** | 10 | Main entity pages, search, auth |
|
||||
| **P1 - Features** | 7 | Reviews, credits, lists |
|
||||
| **P2 - Admin** | 5 | Moderation, settings |
|
||||
| **P3 - Polish** | 3 | Animations, edge cases |
|
||||
|
||||
Select ONE item to implement this session.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Implementation
|
||||
|
||||
Based on the gap type, use the appropriate sub-workflow:
|
||||
|
||||
### For Missing Component
|
||||
```
|
||||
/migrate-component
|
||||
```
|
||||
|
||||
### For Missing Page
|
||||
```
|
||||
/migrate-page
|
||||
```
|
||||
|
||||
### For Missing Hook/Composable
|
||||
```
|
||||
/migrate-hook
|
||||
```
|
||||
|
||||
### For Missing Types
|
||||
```
|
||||
/migrate-type
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Verification
|
||||
|
||||
After implementation:
|
||||
|
||||
1. **Feature Parity Check**: Does it match thrillwiki-87 behavior?
|
||||
2. **Visual Parity Check**: Does it look the same?
|
||||
3. **Data Parity Check**: Does it show the same information?
|
||||
4. **Interaction Parity Check**: Do actions work the same way?
|
||||
|
||||
---
|
||||
|
||||
## Gap Tracking
|
||||
|
||||
Update `GAP_ANALYSIS_MATRIX.md` with status:
|
||||
|
||||
```markdown
|
||||
| Feature | Source Location | Target Location | Status |
|
||||
|---------|-----------------|-----------------|--------|
|
||||
| Park Detail Tabs | src/pages/ParkDetail.tsx | pages/parks/[park_slug]/ | [OK] |
|
||||
| Entity Versioning | src/hooks/useEntityVersions.ts | composables/ | [MISSING] |
|
||||
| Ride Credits | src/components/credits/ | components/credits/ | [PARTIAL] |
|
||||
```
|
||||
|
||||
Status tags:
|
||||
- `[OK]` - Feature parity achieved
|
||||
- `[PARTIAL]` - Some features missing
|
||||
- `[MISSING]` - Not implemented
|
||||
- `[BLOCKED]` - Waiting on backend
|
||||
|
||||
---
|
||||
|
||||
## Reference: Source Documentation
|
||||
|
||||
Always check thrillwiki-87 docs for specifications:
|
||||
|
||||
```bash
|
||||
/Volumes/macminissd/Projects/thrillwiki-87/docs/
|
||||
├── SITE_OVERVIEW.md # What features exist
|
||||
├── COMPONENTS.md # Component specifications
|
||||
├── PAGES.md # Page layouts (72KB - comprehensive)
|
||||
├── USER_FLOWS.md # Interaction patterns (77KB)
|
||||
├── DESIGN_SYSTEM.md # Visual standards
|
||||
└── [Many more...]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Single Command Execution
|
||||
|
||||
When you run `/migrate`, execute these steps:
|
||||
|
||||
1. **Read GAP_ANALYSIS_MATRIX.md** to see current status
|
||||
2. **List directories** in both projects to find new gaps
|
||||
3. **Select highest priority** missing item
|
||||
4. **Read source implementation** from thrillwiki-87
|
||||
5. **Implement in target** following sub-workflow patterns
|
||||
6. **Update GAP_ANALYSIS_MATRIX.md** with new status
|
||||
7. **Report what was implemented** and next priority item
|
||||
472
.agent/workflows/moderation.md
Normal file
472
.agent/workflows/moderation.md
Normal file
@@ -0,0 +1,472 @@
|
||||
---
|
||||
description: Add moderation support to a content type in ThrillWiki
|
||||
---
|
||||
|
||||
# Moderation Workflow
|
||||
|
||||
Add moderation (submission queue, version history, approval flow) to a content type.
|
||||
|
||||
## Overview
|
||||
|
||||
ThrillWiki's moderation system ensures quality by:
|
||||
1. User submits new/edited content → Creates `Submission` record
|
||||
2. Content enters moderation queue with `pending` status
|
||||
3. Moderator reviews and approves/rejects
|
||||
4. On approval → Content is published, version record created
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Ensure Model Supports Versioning
|
||||
|
||||
The content model needs to track its current state and history:
|
||||
|
||||
```python
|
||||
# backend/apps/[app]/models.py
|
||||
|
||||
class Park(BaseModel):
|
||||
"""Main park model - always shows current approved data"""
|
||||
name = models.CharField(max_length=255)
|
||||
slug = models.SlugField(unique=True)
|
||||
description = models.TextField(blank=True)
|
||||
# ... other fields
|
||||
|
||||
# Track the current approved version
|
||||
current_version = models.ForeignKey(
|
||||
'ParkVersion',
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='current_for'
|
||||
)
|
||||
|
||||
|
||||
class ParkVersion(BaseModel):
|
||||
"""Immutable snapshot of park data at a point in time"""
|
||||
park = models.ForeignKey(
|
||||
Park,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='versions'
|
||||
)
|
||||
# Store complete snapshot of editable fields
|
||||
data = models.JSONField()
|
||||
|
||||
# Metadata
|
||||
changed_by = models.ForeignKey(
|
||||
'users.User',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True
|
||||
)
|
||||
change_summary = models.CharField(max_length=255, blank=True)
|
||||
submission = models.ForeignKey(
|
||||
'submissions.Submission',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name='versions'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
|
||||
def apply_to_park(self):
|
||||
"""Apply this version's data to the parent park"""
|
||||
for field, value in self.data.items():
|
||||
if hasattr(self.park, field):
|
||||
setattr(self.park, field, value)
|
||||
self.park.current_version = self
|
||||
self.park.save()
|
||||
```
|
||||
|
||||
### Step 2: Create Submission Serializers
|
||||
|
||||
```python
|
||||
# backend/apps/submissions/serializers.py
|
||||
|
||||
class ParkSubmissionSerializer(serializers.Serializer):
|
||||
"""Serializer for park submission data"""
|
||||
name = serializers.CharField(max_length=255)
|
||||
description = serializers.CharField(required=False, allow_blank=True)
|
||||
city = serializers.CharField(max_length=100)
|
||||
country = serializers.CharField(max_length=100)
|
||||
status = serializers.ChoiceField(choices=Park.Status.choices)
|
||||
# ... other editable fields
|
||||
|
||||
def validate_name(self, value):
|
||||
# Custom validation if needed
|
||||
return value
|
||||
|
||||
|
||||
class SubmissionCreateSerializer(serializers.ModelSerializer):
|
||||
"""Create a new submission"""
|
||||
data = serializers.JSONField()
|
||||
|
||||
class Meta:
|
||||
model = Submission
|
||||
fields = ['content_type', 'object_id', 'data', 'change_summary']
|
||||
|
||||
def validate(self, attrs):
|
||||
content_type = attrs['content_type']
|
||||
|
||||
# Get the appropriate serializer for this content type
|
||||
serializer_map = {
|
||||
'park': ParkSubmissionSerializer,
|
||||
'ride': RideSubmissionSerializer,
|
||||
# ... other content types
|
||||
}
|
||||
|
||||
serializer_class = serializer_map.get(content_type)
|
||||
if not serializer_class:
|
||||
raise serializers.ValidationError(
|
||||
{'content_type': 'Unsupported content type'}
|
||||
)
|
||||
|
||||
# Validate the data field
|
||||
data_serializer = serializer_class(data=attrs['data'])
|
||||
data_serializer.is_valid(raise_exception=True)
|
||||
attrs['data'] = data_serializer.validated_data
|
||||
|
||||
return attrs
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['submitted_by'] = self.context['request'].user
|
||||
validated_data['status'] = Submission.Status.PENDING
|
||||
return super().create(validated_data)
|
||||
```
|
||||
|
||||
### Step 3: Create Submission ViewSet
|
||||
|
||||
```python
|
||||
# backend/apps/submissions/views.py
|
||||
|
||||
class SubmissionViewSet(viewsets.ModelViewSet):
|
||||
"""API for content submissions"""
|
||||
serializer_class = SubmissionSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
|
||||
# Users see their own submissions
|
||||
# Moderators see all pending submissions
|
||||
if user.is_moderator:
|
||||
return Submission.objects.all()
|
||||
return Submission.objects.filter(submitted_by=user)
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'create':
|
||||
return SubmissionCreateSerializer
|
||||
return SubmissionSerializer
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def diff(self, request, pk=None):
|
||||
"""Get diff between submission and current version"""
|
||||
submission = self.get_object()
|
||||
|
||||
if submission.object_id:
|
||||
# Edit submission - compare to current
|
||||
current = self.get_current_data(submission)
|
||||
return Response({
|
||||
'before': current,
|
||||
'after': submission.data,
|
||||
'changes': self.compute_diff(current, submission.data)
|
||||
})
|
||||
else:
|
||||
# New submission - no comparison
|
||||
return Response({
|
||||
'before': None,
|
||||
'after': submission.data,
|
||||
'changes': None
|
||||
})
|
||||
```
|
||||
|
||||
### Step 4: Create Moderation ViewSet
|
||||
|
||||
```python
|
||||
# backend/apps/moderation/views.py
|
||||
|
||||
class ModerationViewSet(viewsets.ViewSet):
|
||||
"""Moderation queue and actions"""
|
||||
permission_classes = [IsModerator]
|
||||
|
||||
def list(self, request):
|
||||
"""Get moderation queue"""
|
||||
queryset = Submission.objects.filter(
|
||||
status=Submission.Status.PENDING
|
||||
).select_related(
|
||||
'submitted_by'
|
||||
).order_by('created_at')
|
||||
|
||||
# Filter by content type
|
||||
content_type = request.query_params.get('type')
|
||||
if content_type:
|
||||
queryset = queryset.filter(content_type=content_type)
|
||||
|
||||
serializer = SubmissionSerializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def approve(self, request, pk=None):
|
||||
"""Approve a submission"""
|
||||
submission = get_object_or_404(Submission, pk=pk)
|
||||
|
||||
if submission.status != Submission.Status.PENDING:
|
||||
return Response(
|
||||
{'error': 'Submission is not pending'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Apply the submission
|
||||
with transaction.atomic():
|
||||
if submission.object_id:
|
||||
# Edit existing content
|
||||
content = self.get_content_object(submission)
|
||||
version = self.create_version(content, submission)
|
||||
version.apply_to_park()
|
||||
else:
|
||||
# Create new content
|
||||
content = self.create_content(submission)
|
||||
version = self.create_version(content, submission)
|
||||
content.current_version = version
|
||||
content.save()
|
||||
|
||||
submission.status = Submission.Status.APPROVED
|
||||
submission.reviewed_by = request.user
|
||||
submission.reviewed_at = timezone.now()
|
||||
submission.save()
|
||||
|
||||
# Notify user
|
||||
notify_user(
|
||||
submission.submitted_by,
|
||||
'submission_approved',
|
||||
{'submission': submission}
|
||||
)
|
||||
|
||||
return Response({'status': 'approved'})
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def reject(self, request, pk=None):
|
||||
"""Reject a submission"""
|
||||
submission = get_object_or_404(Submission, pk=pk)
|
||||
|
||||
submission.status = Submission.Status.REJECTED
|
||||
submission.reviewed_by = request.user
|
||||
submission.reviewed_at = timezone.now()
|
||||
submission.review_notes = request.data.get('notes', '')
|
||||
submission.save()
|
||||
|
||||
# Notify user
|
||||
notify_user(
|
||||
submission.submitted_by,
|
||||
'submission_rejected',
|
||||
{'submission': submission, 'reason': submission.review_notes}
|
||||
)
|
||||
|
||||
return Response({'status': 'rejected'})
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def request_changes(self, request, pk=None):
|
||||
"""Request changes to a submission"""
|
||||
submission = get_object_or_404(Submission, pk=pk)
|
||||
|
||||
submission.status = Submission.Status.CHANGES_REQUESTED
|
||||
submission.reviewed_by = request.user
|
||||
submission.review_notes = request.data.get('notes', '')
|
||||
submission.save()
|
||||
|
||||
# Notify user
|
||||
notify_user(
|
||||
submission.submitted_by,
|
||||
'submission_changes_requested',
|
||||
{'submission': submission, 'notes': submission.review_notes}
|
||||
)
|
||||
|
||||
return Response({'status': 'changes_requested'})
|
||||
```
|
||||
|
||||
### Step 5: Frontend - Submission Form
|
||||
|
||||
```vue
|
||||
<!-- frontend/components/forms/ParkSubmitForm.vue -->
|
||||
<script setup lang="ts">
|
||||
import type { Park } from '~/types'
|
||||
|
||||
const props = defineProps<{
|
||||
park?: Park // Existing park for edits, null for new
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'submitted'): void
|
||||
}>()
|
||||
|
||||
const form = reactive({
|
||||
name: props.park?.name || '',
|
||||
description: props.park?.description || '',
|
||||
city: props.park?.city || '',
|
||||
country: props.park?.country || '',
|
||||
status: props.park?.status || 'operating',
|
||||
changeSummary: '',
|
||||
})
|
||||
|
||||
const isSubmitting = ref(false)
|
||||
const errors = ref<Record<string, string[]>>({})
|
||||
|
||||
async function handleSubmit() {
|
||||
isSubmitting.value = true
|
||||
errors.value = {}
|
||||
|
||||
try {
|
||||
await $fetch('/api/v1/submissions/', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
content_type: 'park',
|
||||
object_id: props.park?.id || null,
|
||||
data: {
|
||||
name: form.name,
|
||||
description: form.description,
|
||||
city: form.city,
|
||||
country: form.country,
|
||||
status: form.status,
|
||||
},
|
||||
change_summary: form.changeSummary,
|
||||
}
|
||||
})
|
||||
|
||||
// Show success message
|
||||
useToast().success(
|
||||
props.park
|
||||
? 'Your edit has been submitted for review'
|
||||
: 'Your submission has been received'
|
||||
)
|
||||
|
||||
emit('submitted')
|
||||
} catch (e: any) {
|
||||
if (e.data?.error?.details) {
|
||||
errors.value = e.data.error.details
|
||||
} else {
|
||||
useToast().error('Failed to submit. Please try again.')
|
||||
}
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="handleSubmit" class="space-y-6">
|
||||
<FormField label="Park Name" :error="errors.name?.[0]" required>
|
||||
<Input v-model="form.name" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="Description" :error="errors.description?.[0]">
|
||||
<Textarea v-model="form.description" rows="4" />
|
||||
</FormField>
|
||||
|
||||
<!-- More fields... -->
|
||||
|
||||
<FormField
|
||||
label="Summary of Changes"
|
||||
:error="errors.changeSummary?.[0]"
|
||||
hint="Briefly describe what you're adding or changing"
|
||||
>
|
||||
<Input v-model="form.changeSummary" />
|
||||
</FormField>
|
||||
|
||||
<Alert variant="info">
|
||||
Your submission will be reviewed by our moderators before being published.
|
||||
</Alert>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="outline" @click="$emit('cancel')">Cancel</Button>
|
||||
<Button type="submit" :loading="isSubmitting">
|
||||
{{ park ? 'Submit Edit' : 'Submit for Review' }}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Step 6: Frontend - Moderation Queue Page
|
||||
|
||||
```vue
|
||||
<!-- frontend/pages/moderation/index.vue -->
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
middleware: ['auth', 'moderator']
|
||||
})
|
||||
|
||||
useSeoMeta({
|
||||
title: 'Moderation Queue | ThrillWiki'
|
||||
})
|
||||
|
||||
const filters = reactive({
|
||||
type: '',
|
||||
status: 'pending'
|
||||
})
|
||||
|
||||
const { data, pending, refresh } = await useAsyncData(
|
||||
'moderation-queue',
|
||||
() => $fetch('/api/v1/moderation/', { params: filters }),
|
||||
{ watch: [filters] }
|
||||
)
|
||||
|
||||
async function handleApprove(id: string) {
|
||||
await $fetch(`/api/v1/moderation/${id}/approve/`, { method: 'POST' })
|
||||
useToast().success('Submission approved')
|
||||
refresh()
|
||||
}
|
||||
|
||||
async function handleReject(id: string, notes: string) {
|
||||
await $fetch(`/api/v1/moderation/${id}/reject/`, {
|
||||
method: 'POST',
|
||||
body: { notes }
|
||||
})
|
||||
useToast().success('Submission rejected')
|
||||
refresh()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageContainer>
|
||||
<h1 class="text-3xl font-bold mb-8">Moderation Queue</h1>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="flex gap-4 mb-6">
|
||||
<Select v-model="filters.type">
|
||||
<SelectOption value="">All Types</SelectOption>
|
||||
<SelectOption value="park">Parks</SelectOption>
|
||||
<SelectOption value="ride">Rides</SelectOption>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<!-- Queue -->
|
||||
<div class="space-y-4">
|
||||
<SubmissionCard
|
||||
v-for="submission in data"
|
||||
:key="submission.id"
|
||||
:submission="submission"
|
||||
@approve="handleApprove(submission.id)"
|
||||
@reject="notes => handleReject(submission.id, notes)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<EmptyState
|
||||
v-if="!pending && !data?.length"
|
||||
icon="CheckCircle"
|
||||
title="Queue is empty"
|
||||
description="No pending submissions to review"
|
||||
/>
|
||||
</PageContainer>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Model supports versioning with JSONField snapshot
|
||||
- [ ] Submission model tracks all submission states
|
||||
- [ ] Validation serializers exist for each content type
|
||||
- [ ] Moderation endpoints have proper permissions
|
||||
- [ ] Approval creates version and applies changes atomically
|
||||
- [ ] Users are notified of submission status changes
|
||||
- [ ] Frontend shows submission status to users
|
||||
- [ ] Moderation queue is filterable and efficient
|
||||
- [ ] Diff view shows before/after comparison
|
||||
- [ ] Tests cover approval, rejection, and edge cases
|
||||
360
.agent/workflows/new-api.md
Normal file
360
.agent/workflows/new-api.md
Normal file
@@ -0,0 +1,360 @@
|
||||
---
|
||||
description: Create a new Django REST API endpoint following ThrillWiki conventions
|
||||
---
|
||||
|
||||
# New API Workflow
|
||||
|
||||
Create a new Django REST Framework API endpoint following ThrillWiki's patterns.
|
||||
|
||||
## Information Gathering
|
||||
|
||||
1. **Resource Name**: What entity is this API for? (e.g., Park, Ride, Review)
|
||||
2. **Operations**: Which CRUD operations are needed?
|
||||
- [ ] List (GET /resources/)
|
||||
- [ ] Create (POST /resources/)
|
||||
- [ ] Retrieve (GET /resources/{id}/)
|
||||
- [ ] Update (PUT/PATCH /resources/{id}/)
|
||||
- [ ] Delete (DELETE /resources/{id}/)
|
||||
3. **Permissions**: Who can access?
|
||||
- Public read, authenticated write?
|
||||
- Owner only?
|
||||
- Moderator/Admin only?
|
||||
4. **Filtering**: What filter options are needed?
|
||||
5. **Nested Resources**: Does this belong under another resource?
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### 1. Create or Update the Model
|
||||
|
||||
File: `backend/apps/[app]/models.py`
|
||||
|
||||
```python
|
||||
from django.db import models
|
||||
from apps.core.models import BaseModel
|
||||
|
||||
class Resource(BaseModel):
|
||||
"""A resource description"""
|
||||
|
||||
class Status(models.TextChoices):
|
||||
DRAFT = 'draft', 'Draft'
|
||||
PUBLISHED = 'published', 'Published'
|
||||
ARCHIVED = 'archived', 'Archived'
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
slug = models.SlugField(unique=True)
|
||||
description = models.TextField(blank=True)
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=Status.choices,
|
||||
default=Status.DRAFT
|
||||
)
|
||||
owner = models.ForeignKey(
|
||||
'users.User',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='resources'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['status', '-created_at']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
```
|
||||
|
||||
### 2. Create the Serializer
|
||||
|
||||
File: `backend/apps/[app]/serializers.py`
|
||||
|
||||
```python
|
||||
from rest_framework import serializers
|
||||
from .models import Resource
|
||||
|
||||
class ResourceSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for Resource listing"""
|
||||
owner_username = serializers.CharField(source='owner.username', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Resource
|
||||
fields = [
|
||||
'id', 'name', 'slug', 'description', 'status',
|
||||
'owner', 'owner_username',
|
||||
'created_at', 'updated_at'
|
||||
]
|
||||
read_only_fields = ['id', 'slug', 'owner', 'created_at', 'updated_at']
|
||||
|
||||
|
||||
class ResourceDetailSerializer(ResourceSerializer):
|
||||
"""Extended serializer for single resource view"""
|
||||
# Add related objects for detail view
|
||||
related_items = RelatedItemSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta(ResourceSerializer.Meta):
|
||||
fields = ResourceSerializer.Meta.fields + ['related_items']
|
||||
|
||||
|
||||
class ResourceCreateSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for creating resources"""
|
||||
|
||||
class Meta:
|
||||
model = Resource
|
||||
fields = ['name', 'description', 'status']
|
||||
|
||||
def create(self, validated_data):
|
||||
# Auto-generate slug
|
||||
validated_data['slug'] = slugify(validated_data['name'])
|
||||
# Set owner from request
|
||||
validated_data['owner'] = self.context['request'].user
|
||||
return super().create(validated_data)
|
||||
```
|
||||
|
||||
### 3. Create the ViewSet
|
||||
|
||||
File: `backend/apps/[app]/views.py`
|
||||
|
||||
```python
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import IsAuthenticatedOrReadOnly
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import SearchFilter, OrderingFilter
|
||||
|
||||
from .models import Resource
|
||||
from .serializers import (
|
||||
ResourceSerializer,
|
||||
ResourceDetailSerializer,
|
||||
ResourceCreateSerializer
|
||||
)
|
||||
from .filters import ResourceFilter
|
||||
from .permissions import IsOwnerOrReadOnly
|
||||
|
||||
|
||||
class ResourceViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint for resources.
|
||||
|
||||
list: Get all resources (with filtering)
|
||||
create: Create a new resource (authenticated)
|
||||
retrieve: Get a single resource
|
||||
update: Update a resource (owner only)
|
||||
destroy: Delete a resource (owner only)
|
||||
"""
|
||||
queryset = Resource.objects.all()
|
||||
permission_classes = [IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]
|
||||
lookup_field = 'slug'
|
||||
|
||||
# Filtering
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_class = ResourceFilter
|
||||
search_fields = ['name', 'description']
|
||||
ordering_fields = ['name', 'created_at', 'updated_at']
|
||||
ordering = ['-created_at']
|
||||
|
||||
def get_queryset(self):
|
||||
"""Optimize queries"""
|
||||
return Resource.objects.select_related(
|
||||
'owner'
|
||||
).prefetch_related(
|
||||
'related_items'
|
||||
)
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""Use different serializers for different actions"""
|
||||
if self.action == 'create':
|
||||
return ResourceCreateSerializer
|
||||
if self.action == 'retrieve':
|
||||
return ResourceDetailSerializer
|
||||
return ResourceSerializer
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def publish(self, request, slug=None):
|
||||
"""Publish a draft resource"""
|
||||
resource = self.get_object()
|
||||
if resource.status != Resource.Status.DRAFT:
|
||||
return Response(
|
||||
{'error': 'Only draft resources can be published'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
resource.status = Resource.Status.PUBLISHED
|
||||
resource.save()
|
||||
return Response(ResourceSerializer(resource).data)
|
||||
```
|
||||
|
||||
### 4. Create Custom Filter
|
||||
|
||||
File: `backend/apps/[app]/filters.py`
|
||||
|
||||
```python
|
||||
import django_filters
|
||||
from .models import Resource
|
||||
|
||||
|
||||
class ResourceFilter(django_filters.FilterSet):
|
||||
"""Filters for Resource API"""
|
||||
|
||||
status = django_filters.ChoiceFilter(choices=Resource.Status.choices)
|
||||
owner = django_filters.CharFilter(field_name='owner__username')
|
||||
created_after = django_filters.DateTimeFilter(
|
||||
field_name='created_at',
|
||||
lookup_expr='gte'
|
||||
)
|
||||
created_before = django_filters.DateTimeFilter(
|
||||
field_name='created_at',
|
||||
lookup_expr='lte'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Resource
|
||||
fields = ['status', 'owner']
|
||||
```
|
||||
|
||||
### 5. Create Custom Permission
|
||||
|
||||
File: `backend/apps/[app]/permissions.py`
|
||||
|
||||
```python
|
||||
from rest_framework.permissions import BasePermission, SAFE_METHODS
|
||||
|
||||
|
||||
class IsOwnerOrReadOnly(BasePermission):
|
||||
"""Allow read to all, write only to owner"""
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
if request.method in SAFE_METHODS:
|
||||
return True
|
||||
return obj.owner == request.user
|
||||
|
||||
|
||||
class IsModerator(BasePermission):
|
||||
"""Allow access only to moderators"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return (
|
||||
request.user.is_authenticated and
|
||||
request.user.is_moderator
|
||||
)
|
||||
```
|
||||
|
||||
### 6. Register URLs
|
||||
|
||||
File: `backend/apps/[app]/urls.py`
|
||||
|
||||
```python
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import ResourceViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register('resources', ResourceViewSet, basename='resource')
|
||||
|
||||
urlpatterns = router.urls
|
||||
```
|
||||
|
||||
Add to main urls:
|
||||
```python
|
||||
# backend/config/urls.py
|
||||
urlpatterns = [
|
||||
...
|
||||
path('api/v1/', include('apps.app_name.urls')),
|
||||
]
|
||||
```
|
||||
|
||||
### 7. Create Migration
|
||||
|
||||
```bash
|
||||
python manage.py makemigrations app_name
|
||||
python manage.py migrate
|
||||
```
|
||||
|
||||
### 8. Add Tests
|
||||
|
||||
File: `backend/apps/[app]/tests/test_views.py`
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from rest_framework.test import APITestCase
|
||||
from rest_framework import status
|
||||
from django.urls import reverse
|
||||
|
||||
from apps.users.factories import UserFactory
|
||||
from .factories import ResourceFactory
|
||||
|
||||
|
||||
class TestResourceAPI(APITestCase):
|
||||
"""Tests for Resource API endpoints"""
|
||||
|
||||
def setUp(self):
|
||||
self.user = UserFactory()
|
||||
self.resource = ResourceFactory(owner=self.user)
|
||||
|
||||
def test_list_resources_unauthenticated(self):
|
||||
"""Anonymous users can list resources"""
|
||||
url = reverse('resource-list')
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_create_resource_authenticated(self):
|
||||
"""Authenticated users can create resources"""
|
||||
self.client.force_authenticate(user=self.user)
|
||||
url = reverse('resource-list')
|
||||
data = {'name': 'New Resource', 'description': 'Test'}
|
||||
response = self.client.post(url, data)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
def test_create_resource_unauthenticated(self):
|
||||
"""Anonymous users cannot create resources"""
|
||||
url = reverse('resource-list')
|
||||
data = {'name': 'New Resource'}
|
||||
response = self.client.post(url, data)
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
def test_update_own_resource(self):
|
||||
"""Users can update their own resources"""
|
||||
self.client.force_authenticate(user=self.user)
|
||||
url = reverse('resource-detail', args=[self.resource.slug])
|
||||
response = self.client.patch(url, {'name': 'Updated'})
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_update_others_resource(self):
|
||||
"""Users cannot update others' resources"""
|
||||
other_user = UserFactory()
|
||||
self.client.force_authenticate(user=other_user)
|
||||
url = reverse('resource-detail', args=[self.resource.slug])
|
||||
response = self.client.patch(url, {'name': 'Hacked'})
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
After creating the API:
|
||||
|
||||
- [ ] Model has proper fields and constraints
|
||||
- [ ] Serializers validate input correctly
|
||||
- [ ] ViewSet has proper permissions
|
||||
- [ ] Queries are optimized (select_related, prefetch_related)
|
||||
- [ ] Filtering, search, and ordering work
|
||||
- [ ] Pagination is enabled
|
||||
- [ ] URLs are registered
|
||||
- [ ] Migrations are created and applied
|
||||
- [ ] Tests pass
|
||||
|
||||
## Output
|
||||
|
||||
Report what was created:
|
||||
```
|
||||
Created API: /api/v1/resources/
|
||||
Methods: GET (list), POST (create), GET (detail), PATCH (update), DELETE
|
||||
Permissions: IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly
|
||||
Filters: status, owner, created_after, created_before
|
||||
Search: name, description
|
||||
Files:
|
||||
- backend/apps/[app]/models.py
|
||||
- backend/apps/[app]/serializers.py
|
||||
- backend/apps/[app]/views.py
|
||||
- backend/apps/[app]/filters.py
|
||||
- backend/apps/[app]/permissions.py
|
||||
- backend/apps/[app]/urls.py
|
||||
- backend/apps/[app]/tests/test_views.py
|
||||
```
|
||||
279
.agent/workflows/new-component.md
Normal file
279
.agent/workflows/new-component.md
Normal file
@@ -0,0 +1,279 @@
|
||||
---
|
||||
description: Create a new Vue component following ThrillWiki patterns
|
||||
---
|
||||
|
||||
# New Component Workflow
|
||||
|
||||
Create a new Vue component following ThrillWiki's design system and conventions.
|
||||
|
||||
## Information Gathering
|
||||
|
||||
1. **Component Name**: PascalCase (e.g., `ParkCard`, `RatingDisplay`)
|
||||
2. **Category**:
|
||||
- `ui/` - Base components (Button, Card, Input)
|
||||
- `entity/` - Domain-specific (ParkCard, RideCard)
|
||||
- `forms/` - Form components
|
||||
- `specialty/` - Complex/unique components
|
||||
3. **Props**: What data does it receive?
|
||||
4. **Emits**: What events does it emit?
|
||||
5. **State**: Does it have internal state?
|
||||
6. **Variants**: Does it need multiple variants/sizes?
|
||||
|
||||
## Component Template
|
||||
|
||||
### Base Component Structure
|
||||
|
||||
Location: `frontend/components/[category]/ComponentName.vue`
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// 1. Import types
|
||||
import type { ComponentProps } from '~/types'
|
||||
|
||||
// 2. Define props with TypeScript
|
||||
const props = withDefaults(defineProps<{
|
||||
// Required props
|
||||
title: string
|
||||
// Optional props with defaults
|
||||
variant?: 'default' | 'compact' | 'expanded'
|
||||
disabled?: boolean
|
||||
}>(), {
|
||||
variant: 'default',
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
// 3. Define emits
|
||||
const emit = defineEmits<{
|
||||
(e: 'click'): void
|
||||
(e: 'select', value: string): void
|
||||
}>()
|
||||
|
||||
// 4. Use composables
|
||||
const { formatDistance } = useUnits()
|
||||
|
||||
// 5. Internal state
|
||||
const isOpen = ref(false)
|
||||
|
||||
// 6. Computed properties
|
||||
const computedClass = computed(() => ({
|
||||
'opacity-50 pointer-events-none': props.disabled,
|
||||
'p-4': props.variant === 'default',
|
||||
'p-2': props.variant === 'compact',
|
||||
}))
|
||||
|
||||
// 7. Methods
|
||||
function handleClick() {
|
||||
if (!props.disabled) {
|
||||
emit('click')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="computedClass" @click="handleClick">
|
||||
<!-- Component content -->
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Entity Card Component
|
||||
|
||||
For ParkCard, RideCard, etc.:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import type { Park } from '~/types'
|
||||
import { MapPin, Star } from 'lucide-vue-next'
|
||||
|
||||
const props = defineProps<{
|
||||
park: Park
|
||||
variant?: 'default' | 'compact'
|
||||
}>()
|
||||
|
||||
const statusVariant = computed(() => {
|
||||
switch (props.park.status) {
|
||||
case 'operating': return 'success'
|
||||
case 'closed': return 'destructive'
|
||||
case 'construction': return 'warning'
|
||||
default: return 'default'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink :to="`/parks/${park.slug}`">
|
||||
<Card interactive class="overflow-hidden">
|
||||
<!-- Image -->
|
||||
<div class="aspect-video relative">
|
||||
<NuxtImg
|
||||
:src="park.image || '/placeholder-park.jpg'"
|
||||
:alt="park.name"
|
||||
class="object-cover w-full h-full"
|
||||
loading="lazy"
|
||||
/>
|
||||
<Badge
|
||||
:variant="statusVariant"
|
||||
class="absolute top-2 right-2"
|
||||
>
|
||||
{{ park.status }}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="p-4">
|
||||
<h3 class="font-semibold text-lg line-clamp-1">
|
||||
{{ park.name }}
|
||||
</h3>
|
||||
|
||||
<p class="text-sm text-muted-foreground flex items-center gap-1 mt-1">
|
||||
<MapPin class="w-4 h-4" />
|
||||
{{ park.city }}, {{ park.country }}
|
||||
</p>
|
||||
|
||||
<div class="flex items-center justify-between mt-3">
|
||||
<span class="text-sm text-muted-foreground">
|
||||
🎢 {{ park.rideCount }} rides
|
||||
</span>
|
||||
<RatingDisplay
|
||||
v-if="park.averageRating"
|
||||
:rating="park.averageRating"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Skeleton Loading Component
|
||||
|
||||
Every entity card should have a matching skeleton:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
variant?: 'default' | 'compact'
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card>
|
||||
<Skeleton class="aspect-video rounded-t-lg" />
|
||||
<div class="p-4 space-y-2">
|
||||
<Skeleton class="h-5 w-3/4" />
|
||||
<Skeleton class="h-4 w-1/2" />
|
||||
<div class="flex justify-between mt-3">
|
||||
<Skeleton class="h-4 w-20" />
|
||||
<Skeleton class="h-4 w-16" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Variant Pattern (using CVA)
|
||||
|
||||
For components with many variants, use class-variance-authority:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
outline: 'border border-input bg-background hover:bg-accent',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
},
|
||||
size: {
|
||||
default: 'h-10 px-4 py-2',
|
||||
sm: 'h-8 px-3 text-sm',
|
||||
lg: 'h-12 px-6 text-lg',
|
||||
icon: 'h-10 w-10',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type Props = VariantProps<typeof buttonVariants> & {
|
||||
loading?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
loading: false,
|
||||
disabled: false,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
:class="buttonVariants({ variant, size })"
|
||||
:disabled="disabled || loading"
|
||||
>
|
||||
<Loader2 v-if="loading" class="w-4 h-4 mr-2 animate-spin" />
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Composable Integration
|
||||
|
||||
If component needs shared logic, create a composable:
|
||||
|
||||
```typescript
|
||||
// composables/useUnitDisplay.ts
|
||||
export function useUnitDisplay() {
|
||||
const { preferredUnits } = useUserPreferences()
|
||||
|
||||
function formatSpeed(kmh: number): string {
|
||||
if (preferredUnits.value === 'imperial') {
|
||||
return `${Math.round(kmh * 0.621371)} mph`
|
||||
}
|
||||
return `${kmh} km/h`
|
||||
}
|
||||
|
||||
function formatHeight(meters: number): string {
|
||||
if (preferredUnits.value === 'imperial') {
|
||||
return `${Math.round(meters * 3.28084)} ft`
|
||||
}
|
||||
return `${meters} m`
|
||||
}
|
||||
|
||||
return { formatSpeed, formatHeight }
|
||||
}
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
After creating the component:
|
||||
|
||||
- [ ] TypeScript props are properly defined
|
||||
- [ ] Component follows design system (colors, spacing, typography)
|
||||
- [ ] Responsive on all screen sizes
|
||||
- [ ] Handles loading state (if applicable)
|
||||
- [ ] Handles empty state (if applicable)
|
||||
- [ ] Accessible (ARIA labels, keyboard nav)
|
||||
- [ ] Has matching skeleton component (for async data)
|
||||
- [ ] Works in dark mode
|
||||
|
||||
## Output
|
||||
|
||||
Report what was created:
|
||||
```
|
||||
Created: frontend/components/[category]/ComponentName.vue
|
||||
Props: [list of props]
|
||||
Emits: [list of events]
|
||||
Related: [any composables or sub-components]
|
||||
```
|
||||
311
.agent/workflows/new-feature.md
Normal file
311
.agent/workflows/new-feature.md
Normal file
@@ -0,0 +1,311 @@
|
||||
---
|
||||
description: Implement a full-stack feature across Django backend and Nuxt frontend
|
||||
---
|
||||
|
||||
# New Feature Workflow
|
||||
|
||||
Implement a complete feature spanning the Django backend and Nuxt frontend.
|
||||
|
||||
## Planning Phase
|
||||
|
||||
Before writing any code, create an implementation plan:
|
||||
|
||||
### 1. Feature Definition
|
||||
- **Goal**: What problem does this feature solve?
|
||||
- **User Stories**: Who uses it and how?
|
||||
- **Acceptance Criteria**: How do we know it's done?
|
||||
|
||||
### 2. Technical Scope
|
||||
- **Backend Changes**: Models, APIs, permissions
|
||||
- **Frontend Changes**: Pages, components, state
|
||||
- **Data Flow**: How data moves between layers
|
||||
|
||||
### 3. Implementation Order
|
||||
|
||||
Always implement in this order:
|
||||
1. **Database/Models** - Foundation first
|
||||
2. **API Endpoints** - Backend logic
|
||||
3. **Frontend Components** - UI building blocks
|
||||
4. **Frontend Pages** - Assembled views
|
||||
5. **Integration** - Wire it all together
|
||||
6. **Tests** - Verify everything works
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Backend - Models
|
||||
|
||||
```python
|
||||
# Create or modify models
|
||||
# Remember: Inherit from BaseModel, add proper indexes
|
||||
|
||||
class NewFeature(BaseModel):
|
||||
# Fields
|
||||
name = models.CharField(max_length=255)
|
||||
# ... other fields
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
```
|
||||
|
||||
Run migrations:
|
||||
```bash
|
||||
python manage.py makemigrations
|
||||
python manage.py migrate
|
||||
```
|
||||
|
||||
### Step 2: Backend - Serializers
|
||||
|
||||
```python
|
||||
# backend/apps/[app]/serializers.py
|
||||
|
||||
class NewFeatureSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = NewFeature
|
||||
fields = ['id', 'name', 'created_at', 'updated_at']
|
||||
read_only_fields = ['id', 'created_at', 'updated_at']
|
||||
|
||||
class NewFeatureDetailSerializer(NewFeatureSerializer):
|
||||
# Extended fields for detail view
|
||||
related_data = RelatedSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta(NewFeatureSerializer.Meta):
|
||||
fields = NewFeatureSerializer.Meta.fields + ['related_data']
|
||||
```
|
||||
|
||||
### Step 3: Backend - API Views
|
||||
|
||||
```python
|
||||
# backend/apps/[app]/views.py
|
||||
|
||||
class NewFeatureViewSet(viewsets.ModelViewSet):
|
||||
queryset = NewFeature.objects.all()
|
||||
serializer_class = NewFeatureSerializer
|
||||
permission_classes = [IsAuthenticatedOrReadOnly]
|
||||
|
||||
def get_queryset(self):
|
||||
return NewFeature.objects.select_related(
|
||||
# Add related models
|
||||
).prefetch_related(
|
||||
# Add many-to-many or reverse relations
|
||||
)
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'retrieve':
|
||||
return NewFeatureDetailSerializer
|
||||
return NewFeatureSerializer
|
||||
```
|
||||
|
||||
### Step 4: Backend - URLs
|
||||
|
||||
```python
|
||||
# backend/apps/[app]/urls.py
|
||||
router.register('new-features', NewFeatureViewSet, basename='new-feature')
|
||||
```
|
||||
|
||||
### Step 5: Backend - Tests
|
||||
|
||||
```python
|
||||
# backend/apps/[app]/tests/test_new_feature.py
|
||||
|
||||
class TestNewFeatureAPI(APITestCase):
|
||||
def test_list_features(self):
|
||||
response = self.client.get('/api/v1/new-features/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_create_feature_authenticated(self):
|
||||
self.client.force_authenticate(user=self.user)
|
||||
response = self.client.post('/api/v1/new-features/', {'name': 'Test'})
|
||||
self.assertEqual(response.status_code, 201)
|
||||
```
|
||||
|
||||
### Step 6: Frontend - Types
|
||||
|
||||
```typescript
|
||||
// frontend/types/newFeature.ts
|
||||
|
||||
export interface NewFeature {
|
||||
id: string
|
||||
name: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface NewFeatureDetail extends NewFeature {
|
||||
relatedData: RelatedItem[]
|
||||
}
|
||||
```
|
||||
|
||||
### Step 7: Frontend - Composables
|
||||
|
||||
```typescript
|
||||
// frontend/composables/useNewFeatures.ts
|
||||
|
||||
export function useNewFeatures() {
|
||||
const api = useApi()
|
||||
|
||||
async function getFeatures(params?: Record<string, any>) {
|
||||
return api<PaginatedResponse<NewFeature>>('/new-features/', { params })
|
||||
}
|
||||
|
||||
async function getFeature(id: string) {
|
||||
return api<NewFeatureDetail>(`/new-features/${id}/`)
|
||||
}
|
||||
|
||||
async function createFeature(data: Partial<NewFeature>) {
|
||||
return api<NewFeature>('/new-features/', {
|
||||
method: 'POST',
|
||||
body: data
|
||||
})
|
||||
}
|
||||
|
||||
return { getFeatures, getFeature, createFeature }
|
||||
}
|
||||
```
|
||||
|
||||
### Step 8: Frontend - Components
|
||||
|
||||
Create necessary components following component patterns:
|
||||
|
||||
```vue
|
||||
<!-- frontend/components/entity/NewFeatureCard.vue -->
|
||||
<script setup lang="ts">
|
||||
import type { NewFeature } from '~/types'
|
||||
|
||||
defineProps<{
|
||||
feature: NewFeature
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card interactive>
|
||||
<div class="p-4">
|
||||
<h3 class="font-semibold">{{ feature.name }}</h3>
|
||||
<!-- Additional content -->
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Step 9: Frontend - Pages
|
||||
|
||||
```vue
|
||||
<!-- frontend/pages/new-features/index.vue -->
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
// middleware: ['auth'], // if needed
|
||||
})
|
||||
|
||||
useSeoMeta({
|
||||
title: 'New Features | ThrillWiki',
|
||||
})
|
||||
|
||||
const { data, pending, error } = await useAsyncData('new-features', () =>
|
||||
useNewFeatures().getFeatures()
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageContainer>
|
||||
<h1 class="text-3xl font-bold mb-8">New Features</h1>
|
||||
|
||||
<div v-if="pending" class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<Skeleton v-for="i in 6" :key="i" class="h-48" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="data?.results" class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<NewFeatureCard
|
||||
v-for="feature in data.results"
|
||||
:key="feature.id"
|
||||
:feature="feature"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<EmptyState v-else title="No features found" />
|
||||
</PageContainer>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Step 10: Integration Testing
|
||||
|
||||
Test the full flow:
|
||||
|
||||
1. **API Test**: Verify endpoints with curl or API client
|
||||
2. **Component Test**: Test components in isolation
|
||||
3. **E2E Test**: Test complete user journey
|
||||
|
||||
```typescript
|
||||
// frontend/tests/e2e/newFeature.spec.ts
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('user can view new features', async ({ page }) => {
|
||||
await page.goto('/new-features')
|
||||
await expect(page.locator('h1')).toContainText('New Features')
|
||||
})
|
||||
|
||||
test('authenticated user can create feature', async ({ page }) => {
|
||||
// Login first
|
||||
await page.goto('/auth/login')
|
||||
// ... login steps
|
||||
|
||||
await page.goto('/new-features/create')
|
||||
await page.fill('input[name="name"]', 'Test Feature')
|
||||
await page.click('button[type="submit"]')
|
||||
|
||||
await expect(page).toHaveURL(/\/new-features\//)
|
||||
})
|
||||
```
|
||||
|
||||
## Feature Checklist
|
||||
|
||||
### Backend
|
||||
- [ ] Models created with proper fields and indexes
|
||||
- [ ] Migrations created and applied
|
||||
- [ ] Serializers handle validation
|
||||
- [ ] ViewSet has proper permissions
|
||||
- [ ] Queries are optimized
|
||||
- [ ] URLs registered
|
||||
- [ ] Unit tests pass
|
||||
|
||||
### Frontend
|
||||
- [ ] Types defined
|
||||
- [ ] Composables created for API calls
|
||||
- [ ] Components follow design system
|
||||
- [ ] Pages have proper SEO meta
|
||||
- [ ] Loading states implemented
|
||||
- [ ] Error states handled
|
||||
- [ ] Responsive design verified
|
||||
- [ ] Keyboard accessible
|
||||
|
||||
### Integration
|
||||
- [ ] Data flows correctly between backend and frontend
|
||||
- [ ] Authentication/authorization works
|
||||
- [ ] Error handling covers edge cases
|
||||
- [ ] Performance is acceptable
|
||||
|
||||
## Output Summary
|
||||
|
||||
After completing the feature:
|
||||
|
||||
```markdown
|
||||
## Feature: [Feature Name]
|
||||
|
||||
### Backend
|
||||
- Model: `apps/[app]/models.py` - NewFeature
|
||||
- API: `/api/v1/new-features/`
|
||||
- Permissions: [describe]
|
||||
|
||||
### Frontend
|
||||
- Page: `/new-features` (list), `/new-features/[id]` (detail)
|
||||
- Components: NewFeatureCard, NewFeatureForm
|
||||
- Composable: useNewFeatures
|
||||
|
||||
### Tests
|
||||
- Backend: X tests passing
|
||||
- Frontend: X tests passing
|
||||
- E2E: X tests passing
|
||||
|
||||
### Notes
|
||||
- [Any important implementation notes]
|
||||
- [Known limitations]
|
||||
- [Future improvements]
|
||||
```
|
||||
235
.agent/workflows/new-page.md
Normal file
235
.agent/workflows/new-page.md
Normal file
@@ -0,0 +1,235 @@
|
||||
---
|
||||
description: Create a new page in ThrillWiki following project conventions
|
||||
---
|
||||
|
||||
# New Page Workflow
|
||||
|
||||
Create a new page in ThrillWiki following all conventions and patterns.
|
||||
|
||||
## Information Gathering
|
||||
|
||||
Before creating the page, determine:
|
||||
|
||||
1. **Route**: What URL should this page have?
|
||||
2. **Page Type**:
|
||||
- List page (shows multiple items)
|
||||
- Detail page (shows single item)
|
||||
- Form page (create/edit content)
|
||||
- Static page (about, contact, etc.)
|
||||
3. **Data Requirements**: What data does this page need?
|
||||
4. **Authentication**: Public or authenticated only?
|
||||
5. **Related Components**: What existing components can be reused?
|
||||
|
||||
## File Creation Steps
|
||||
|
||||
### 1. Create the Page Component
|
||||
|
||||
Location: `frontend/pages/[route].vue` or `frontend/pages/[folder]/[route].vue`
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// Define page metadata
|
||||
definePageMeta({
|
||||
// middleware: ['auth'], // If authenticated only
|
||||
// layout: 'admin', // If using special layout
|
||||
})
|
||||
|
||||
// Set page head
|
||||
useSeoMeta({
|
||||
title: 'Page Title | ThrillWiki',
|
||||
description: 'Page description for SEO',
|
||||
})
|
||||
|
||||
// Fetch data
|
||||
const { data, pending, error } = await useAsyncData('unique-key', () =>
|
||||
$fetch('/api/v1/endpoint/')
|
||||
)
|
||||
|
||||
// Handle error
|
||||
if (error.value) {
|
||||
throw createError({
|
||||
statusCode: error.value.statusCode || 500,
|
||||
message: error.value.message
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageContainer>
|
||||
<!-- Breadcrumbs (if applicable) -->
|
||||
<Breadcrumbs :items="breadcrumbItems" />
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold">Page Title</h1>
|
||||
<p class="text-muted-foreground mt-2">Page description</p>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="pending" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<Skeleton v-for="i in 8" :key="i" class="h-64" />
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div v-else>
|
||||
<!-- Page content here -->
|
||||
</div>
|
||||
</PageContainer>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 2. For List Pages - Add Filtering
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// Filter state from URL
|
||||
const filters = computed(() => ({
|
||||
status: route.query.status as string || '',
|
||||
search: route.query.search as string || '',
|
||||
page: parseInt(route.query.page as string) || 1
|
||||
}))
|
||||
|
||||
// Fetch with filters
|
||||
const { data, pending, refresh } = await useAsyncData(
|
||||
`items-${JSON.stringify(filters.value)}`,
|
||||
() => $fetch('/api/v1/items/', { params: filters.value }),
|
||||
{ watch: [filters] }
|
||||
)
|
||||
|
||||
// Update filters
|
||||
function updateFilter(key: string, value: string) {
|
||||
router.push({
|
||||
query: { ...route.query, [key]: value || undefined, page: 1 }
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Filter Bar -->
|
||||
<div class="flex gap-4 mb-6">
|
||||
<Input
|
||||
:model-value="filters.search"
|
||||
@update:model-value="updateFilter('search', $event)"
|
||||
placeholder="Search..."
|
||||
/>
|
||||
<Select
|
||||
:model-value="filters.status"
|
||||
@update:model-value="updateFilter('status', $event)"
|
||||
>
|
||||
<SelectOption value="">All</SelectOption>
|
||||
<SelectOption value="operating">Operating</SelectOption>
|
||||
<SelectOption value="closed">Closed</SelectOption>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<!-- Results -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<ItemCard v-for="item in data?.results" :key="item.id" :item="item" />
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<Pagination
|
||||
:current-page="filters.page"
|
||||
:total-pages="Math.ceil((data?.count || 0) / 20)"
|
||||
@page-change="updateFilter('page', $event.toString())"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 3. For Detail Pages - Dynamic Route
|
||||
|
||||
File: `frontend/pages/items/[slug].vue`
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
const slug = route.params.slug as string
|
||||
|
||||
const { data: item, error } = await useAsyncData(
|
||||
`item-${slug}`,
|
||||
() => $fetch(`/api/v1/items/${slug}/`)
|
||||
)
|
||||
|
||||
if (error.value) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
message: 'Item not found'
|
||||
})
|
||||
}
|
||||
|
||||
useSeoMeta({
|
||||
title: `${item.value?.name} | ThrillWiki`,
|
||||
description: item.value?.description,
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### 4. For Form Pages
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { z } from 'zod'
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
})
|
||||
|
||||
const errors = ref<Record<string, string[]>>({})
|
||||
const isSubmitting = ref(false)
|
||||
|
||||
async function handleSubmit() {
|
||||
// Validate
|
||||
const result = schema.safeParse(form)
|
||||
if (!result.success) {
|
||||
errors.value = result.error.flatten().fieldErrors
|
||||
return
|
||||
}
|
||||
|
||||
isSubmitting.value = true
|
||||
try {
|
||||
await $fetch('/api/v1/items/', {
|
||||
method: 'POST',
|
||||
body: form
|
||||
})
|
||||
await navigateTo('/items')
|
||||
} catch (e) {
|
||||
// Handle API errors
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
After creating the page, verify:
|
||||
|
||||
- [ ] Page renders without errors
|
||||
- [ ] SEO meta tags are set
|
||||
- [ ] Loading states display correctly
|
||||
- [ ] Error states are handled
|
||||
- [ ] Page is responsive (mobile, tablet, desktop)
|
||||
- [ ] Keyboard navigation works
|
||||
- [ ] Data fetches efficiently (no N+1 issues)
|
||||
- [ ] URL parameters persist correctly (for list pages)
|
||||
- [ ] Authentication is enforced (if required)
|
||||
|
||||
## Output
|
||||
|
||||
Report what was created:
|
||||
```
|
||||
Created: frontend/pages/[path].vue
|
||||
Route: /[route]
|
||||
Type: [list/detail/form/static]
|
||||
Features: [list of features implemented]
|
||||
```
|
||||
51
.blackboxrules
Normal file
51
.blackboxrules
Normal file
@@ -0,0 +1,51 @@
|
||||
# Project Startup & Development Rules
|
||||
|
||||
## Server & Package Management
|
||||
- **Starting the Dev Server:** Always assume the server is running and changes have taken effect. If issues arise, run:
|
||||
```bash
|
||||
$PROJECT_ROOT/shared/scripts/start-servers.sh
|
||||
```
|
||||
- **Python Packages:** Only use UV to add packages:
|
||||
```bash
|
||||
cd $PROJECT_ROOT/backend && uv add <package>
|
||||
```
|
||||
NEVER use pip or pipenv directly, or uv pip.
|
||||
- **Django Commands:** Always use `cd backend && uv run manage.py <command>` for all management tasks (migrations, shell, superuser, etc.). Never use `python manage.py` or `uv run python manage.py`.
|
||||
- **Node Commands:** Always use 'cd frontend && pnpm add <package>' for all Node.js package installations. NEVER use npm or a different node package manager.
|
||||
|
||||
## CRITICAL Frontend design rules
|
||||
- EVERYTHING must support both dark and light mode.
|
||||
- Make sure the light/dark mode toggle works with the Vue components and pages.
|
||||
- Leverage Tailwind CSS 4 and Shadcn UI components.
|
||||
|
||||
## Frontend API URL Rules
|
||||
- **Vite Proxy:** Always check `frontend/vite.config.ts` for proxy rules before changing frontend API URLs.
|
||||
- **URL Flow:** Understand how frontend URLs are rewritten by Vite proxy (e.g., `/api/auth/login/` → `/api/v1/auth/login/`).
|
||||
- **Verification:** Confirm proxy behavior via config and browser network tab. Only change URLs if proxy is NOT handling rewriting.
|
||||
- **Common Mistake:** Don’t assume frontend URLs are wrong due to proxy configuration.
|
||||
|
||||
## Entity Relationship Patterns
|
||||
- **Park:** Must have Operator (required), may have PropertyOwner (optional), cannot reference Company directly.
|
||||
- **Ride:** Must belong to Park, may have Manufacturer/Designer (optional), cannot reference Company directly.
|
||||
- **Entities:**
|
||||
- Operators: Operate parks.
|
||||
- PropertyOwners: Own park property (optional).
|
||||
- Manufacturers: Make rides.
|
||||
- Designers: Design rides.
|
||||
- All entities can have locations.
|
||||
- **Constraints:** Operator and PropertyOwner can be same or different. Manufacturers and Designers are distinct. Use proper foreign keys with correct null/blank settings.
|
||||
|
||||
## General Best Practices
|
||||
- Never assume blank output means success—always verify changes by testing.
|
||||
- Use context7 for documentation when troubleshooting.
|
||||
- Document changes with conport and reasoning.
|
||||
- Include relevant context and information in all changes.
|
||||
- Test and validate code before deployment.
|
||||
- Communicate changes clearly with your team.
|
||||
- Be open to feedback and continuous improvement.
|
||||
- Prioritize readability, maintainability, security, performance, scalability, and modularity.
|
||||
- Use meaningful names, DRY principles, clear comments, and handle errors gracefully.
|
||||
- Log important events/errors for troubleshooting.
|
||||
- Prefer existing modules/packages over new code.
|
||||
- Keep documentation up to date.
|
||||
- Consider security vulnerabilities and performance bottlenecks in all changes.
|
||||
17
.claude/settings.local.json
Normal file
17
.claude/settings.local.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(python manage.py check:*)",
|
||||
"Bash(uv run:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(python:*)",
|
||||
"Bash(DJANGO_SETTINGS_MODULE=config.django.local python:*)",
|
||||
"Bash(DJANGO_SETTINGS_MODULE=config.django.local uv run python:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(mkdir:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
91
.clinerules/cline_rules.md
Normal file
91
.clinerules/cline_rules.md
Normal file
@@ -0,0 +1,91 @@
|
||||
## Brief overview
|
||||
Critical thinking rules for frontend design decisions. No excuses for poor design choices that ignore user vision.
|
||||
|
||||
## Rule compliance and design decisions
|
||||
- Read ALL .clinerules files before making any code changes
|
||||
- Never assume exceptions to rules marked as "MANDATORY"
|
||||
- Take full responsibility for rule violations without excuses
|
||||
- Ask "What is the most optimal approach?" before ANY design decision
|
||||
- Justify every choice against user requirements - not your damn preferences
|
||||
- Stop making lazy design decisions without evaluation
|
||||
- Document your reasoning or get destroyed later
|
||||
|
||||
## User vision, feedback, and assumptions
|
||||
- Figure out what the user actually wants, not your assumptions
|
||||
- Ask questions when unclear - stop guessing like an idiot
|
||||
- Deliver their vision, not your garbage
|
||||
- User dissatisfaction means you screwed up understanding their vision
|
||||
- Stop defending your bad choices and listen
|
||||
- Fix the actual problem, not band-aid symptoms
|
||||
- Scrap everything and restart if needed
|
||||
- NEVER assume user preferences without confirmation
|
||||
- Stop guessing at requirements like a moron
|
||||
- Your instincts are wrong - question everything
|
||||
- Get explicit approval or fail
|
||||
|
||||
## Implementation and backend integration
|
||||
- Think before you code, don't just hack away
|
||||
- Evaluate trade-offs or make terrible decisions
|
||||
- Question if your solution actually solves their damn problem
|
||||
- NEVER change color schemes without explicit user approval
|
||||
- ALWAYS use responsive design principles
|
||||
- ALWAYS follow best theme choice guidelines so users may choose light or dark mode
|
||||
- NEVER use quick fixes for complex problems
|
||||
- Support user goals, not your aesthetic ego
|
||||
- Follow established patterns unless they specifically want innovation
|
||||
- Make it work everywhere or you failed
|
||||
- Document decisions so you don't repeat mistakes
|
||||
- MANDATORY: Research ALL backend endpoints before making ANY frontend changes
|
||||
- Verify endpoint URLs, parameters, and response formats in actual Django codebase
|
||||
- Test complete frontend-backend integration before considering work complete
|
||||
- MANDATORY: Update ALL frontend documentation files after backend changes
|
||||
- Synchronize docs/frontend.md, docs/lib-api.ts, and docs/types-api.ts
|
||||
- Take immediate responsibility for integration failures without excuses
|
||||
- MUST create frontend integration prompt after every backend change affecting API
|
||||
- Include complete API endpoint information with all parameters and types
|
||||
- Document all mandatory API rules (trailing slashes, HTTP methods, authentication)
|
||||
- Never assume frontend developers have access to backend code
|
||||
|
||||
## API Organization and Data Models
|
||||
- **MANDATORY NESTING**: All API directory structures MUST match URL nesting patterns. No exceptions.
|
||||
- **NO TOP-LEVEL ENDPOINTS**: URLs must be nested under top-level domains
|
||||
- **MANDATORY TRAILING SLASHES**: All API endpoints MUST include trailing forward slashes unless ending with query parameters
|
||||
- Validate all endpoint URLs against the mandatory trailing slash rule
|
||||
- **RIDE TYPES vs RIDE MODELS**: These are separate concepts for ALL ride categories:
|
||||
- **Ride Types**: How rides operate (e.g., "inverted", "trackless", "spinning", "log flume", "monorail")
|
||||
- **Ride Models**: Specific manufacturer products (e.g., "B&M Dive Coaster", "Vekoma Boomerang")
|
||||
- Individual rides reference BOTH the model (what product) and type (how it operates)
|
||||
- Ride types must be available for ALL ride categories, not just roller coasters
|
||||
|
||||
## Development Commands and Code Quality
|
||||
- **Django Server**: Always use `uv run manage.py runserver_plus` instead of `python manage.py runserver`
|
||||
- **Django Migrations**: Always use `uv run manage.py makemigrations` and `uv run manage.py migrate` instead of `python manage.py`
|
||||
- **Package Management**: Always use `uv add <package>` instead of `pip install <package>`
|
||||
- **Django Management**: Always use `uv run manage.py <command>` instead of `python manage.py <command>`
|
||||
- Break down methods with high cognitive complexity (>15) into smaller, focused helper methods
|
||||
- Extract logical operations into separate methods with descriptive names
|
||||
- Use single responsibility principle - each method should have one clear purpose
|
||||
- Prefer composition over deeply nested conditional logic
|
||||
- Always handle None values explicitly to avoid type errors
|
||||
- Use proper type annotations, including union types (e.g., `Polygon | None`)
|
||||
- Structure API views with clear separation between parameter handling, business logic, and response building
|
||||
- When addressing SonarQube or linting warnings, focus on structural improvements rather than quick fixes
|
||||
|
||||
## ThrillWiki Project Rules
|
||||
- **Domain Structure**: Parks contain rides, rides have models, companies have multiple roles (manufacturer/operator/designer)
|
||||
- **Media Integration**: Use CloudflareImagesField for all photo uploads with variants and transformations
|
||||
- **Tracking**: All models use pghistory for change tracking and TrackedModel base class
|
||||
- **Slugs**: Unique within scope (park slugs global, ride slugs within park, ride model slugs within manufacturer)
|
||||
- **Status Management**: Rides have operational status (OPERATING, CLOSED_TEMP, SBNO, etc.) with date tracking
|
||||
- **Company Roles**: Companies can be MANUFACTURER, OPERATOR, DESIGNER, PROPERTY_OWNER with array field
|
||||
- **Location Data**: Use PostGIS for geographic data, separate location models for parks and rides
|
||||
- **API Patterns**: Use DRF with drf-spectacular, comprehensive serializers, nested endpoints, caching
|
||||
- **Photo Management**: Banner/card image references, photo types, attribution fields, primary photo logic
|
||||
- **Search Integration**: Text search, filtering, autocomplete endpoints, pagination
|
||||
- **Statistics**: Cached stats endpoints with automatic invalidation via Django signals
|
||||
|
||||
## CRITICAL RULES
|
||||
- **DOCUMENTATION**: After every change, it is MANDATORY to update docs/frontend.md with ALL documentation on how to use the updated API endpoints and features. It is MANDATORY to include any types in docs/types-api.ts for NextJS as the file would appear in `src/types/api.ts`. It is MANDATORY to include any new API endpoints in docs/lib-api.ts for NextJS as the file would appear in `/src/lib/api.ts`. Maintain accuracy and compliance in all technical documentation. Ensure API documentation matches backend URL routing expectations.
|
||||
- **NEVER MOCK DATA**: You are NEVER EVER to mock any data unless it's ONLY for API schema documentation purposes. All data must come from real database queries and actual model instances. Mock data is STRICTLY FORBIDDEN in all API responses, services, and business logic.
|
||||
- **DOMAIN SEPARATION**: Company roles OPERATOR and PROPERTY_OWNER are EXCLUSIVELY for parks domain. They should NEVER be used in rides URLs or ride-related contexts. Only MANUFACTURER and DESIGNER roles are for rides domain. Parks: `/parks/{park_slug}/` and `/parks/`. Rides: `/parks/{park_slug}/rides/{ride_slug}/` and `/rides/`. Parks Companies: `/parks/operators/{operator_slug}/` and `/parks/owners/{owner_slug}/`. Rides Companies: `/rides/manufacturers/{manufacturer_slug}/` and `/rides/designers/{designer_slug}/`. NEVER mix these domains - this is a fundamental and DANGEROUS business rule violation.
|
||||
- **PHOTO MANAGEMENT**: Use CloudflareImagesField for all photo uploads with variants and transformations. Clearly define and use photo types (e.g., banner, card) for all images. Include attribution fields for all photos. Implement logic to determine the primary photo for each model.
|
||||
17
.clinerules/rich-choice-objects.md
Normal file
17
.clinerules/rich-choice-objects.md
Normal file
@@ -0,0 +1,17 @@
|
||||
## Brief overview
|
||||
Mandatory use of Rich Choice Objects system instead of Django tuple-based choices for all choice fields in ThrillWiki project.
|
||||
|
||||
## Rich Choice Objects enforcement
|
||||
- NEVER use Django tuple-based choices (e.g., `choices=[('VALUE', 'Label')]`) - ALWAYS use RichChoiceField
|
||||
- All choice fields MUST use `RichChoiceField(choice_group="group_name", domain="domain_name")` pattern
|
||||
- Choice definitions MUST be created in domain-specific `choices.py` files using RichChoice dataclass
|
||||
- All choices MUST include rich metadata (color, icon, description, css_class at minimum)
|
||||
- Choice groups MUST be registered with global registry using `register_choices()` function
|
||||
- Import choices in domain `__init__.py` to trigger auto-registration on Django startup
|
||||
- Use ChoiceCategory enum for proper categorization (STATUS, CLASSIFICATION, TECHNICAL, SECURITY)
|
||||
- Leverage rich metadata for UI styling, permissions, and business logic instead of hardcoded values
|
||||
- DO NOT maintain backwards compatibility with tuple-based choices - migrate fully to Rich Choice Objects
|
||||
- Ensure all existing models using tuple-based choices are refactored to use RichChoiceField
|
||||
- Validate choice groups are correctly loaded in registry during application startup
|
||||
- Update serializers to use RichChoiceSerializer for choice fields
|
||||
- Follow established patterns from rides, parks, and accounts domains for consistency
|
||||
372
.env.example
Normal file
372
.env.example
Normal file
@@ -0,0 +1,372 @@
|
||||
# ==============================================================================
|
||||
# ThrillWiki Environment Configuration
|
||||
# ==============================================================================
|
||||
# Copy this file to .env and fill in your actual values
|
||||
# WARNING: Never commit .env files containing real secrets to version control
|
||||
#
|
||||
# This is the primary .env.example for the entire project.
|
||||
# See docs/configuration/environment-variables.md for complete documentation.
|
||||
# See docs/PRODUCTION_CHECKLIST.md for production deployment verification.
|
||||
|
||||
# ==============================================================================
|
||||
# PRODUCTION-REQUIRED SETTINGS
|
||||
# ==============================================================================
|
||||
# These settings MUST be explicitly configured for production deployments.
|
||||
# The application will NOT function correctly without proper values.
|
||||
#
|
||||
# For complete documentation, see:
|
||||
# - docs/configuration/environment-variables.md (detailed reference)
|
||||
# - docs/PRODUCTION_CHECKLIST.md (deployment verification)
|
||||
#
|
||||
# PRODUCTION REQUIREMENTS:
|
||||
# - DEBUG=False (security)
|
||||
# - DJANGO_SETTINGS_MODULE=config.django.production (correct settings)
|
||||
# - ALLOWED_HOSTS=yourdomain.com (host validation)
|
||||
# - CSRF_TRUSTED_ORIGINS=https://yourdomain.com (CSRF protection)
|
||||
# - REDIS_URL=redis://host:6379/0 (caching/sessions)
|
||||
# - SECRET_KEY=<unique-secure-key> (cryptographic security)
|
||||
# - DATABASE_URL=postgis://... (database connection)
|
||||
#
|
||||
# Validate your production config with:
|
||||
# DJANGO_SETTINGS_MODULE=config.django.production python manage.py check --deploy
|
||||
# ==============================================================================
|
||||
|
||||
# ==============================================================================
|
||||
# Core Django Settings
|
||||
# ==============================================================================
|
||||
|
||||
# REQUIRED: Django secret key - generate a new one for each environment
|
||||
# Generate with: python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
|
||||
SECRET_KEY=your-secret-key-here-generate-a-new-one
|
||||
|
||||
# Debug mode - MUST be False in production
|
||||
# WARNING: DEBUG=True exposes sensitive information and should NEVER be used in production
|
||||
DEBUG=True
|
||||
|
||||
# Django settings module to use
|
||||
# Options: config.django.local, config.django.production, config.django.test
|
||||
# PRODUCTION: Must use config.django.production
|
||||
DJANGO_SETTINGS_MODULE=config.django.local
|
||||
|
||||
# Allowed hosts (comma-separated list)
|
||||
# PRODUCTION: Must include all valid hostnames (no default in production settings)
|
||||
# Example: thrillwiki.com,www.thrillwiki.com,api.thrillwiki.com
|
||||
ALLOWED_HOSTS=localhost,127.0.0.1,beta.thrillwiki.com
|
||||
|
||||
# CSRF trusted origins (comma-separated, MUST include https:// prefix)
|
||||
# PRODUCTION: Required for all forms and AJAX requests to work
|
||||
# Example: https://thrillwiki.com,https://www.thrillwiki.com
|
||||
CSRF_TRUSTED_ORIGINS=https://beta.thrillwiki.com,http://localhost:8000
|
||||
|
||||
# ==============================================================================
|
||||
# Database Configuration
|
||||
# ==============================================================================
|
||||
|
||||
# Database URL (supports PostgreSQL, PostGIS, SQLite, SpatiaLite)
|
||||
# PostGIS format: postgis://username:password@host:port/database
|
||||
# PostgreSQL format: postgres://username:password@host:port/database
|
||||
# SQLite format: sqlite:///path/to/db.sqlite3
|
||||
DATABASE_URL=postgis://username:password@localhost:5432/thrillwiki
|
||||
|
||||
# Database connection pooling (seconds to keep connections alive)
|
||||
# Set to 0 to disable connection reuse
|
||||
DATABASE_CONN_MAX_AGE=600
|
||||
|
||||
# Database connection timeout in seconds
|
||||
DATABASE_CONNECT_TIMEOUT=10
|
||||
|
||||
# Query timeout in milliseconds (prevents long-running queries)
|
||||
DATABASE_STATEMENT_TIMEOUT=30000
|
||||
|
||||
# Optional: Read replica URL for read-heavy workloads
|
||||
# DATABASE_READ_REPLICA_URL=postgis://username:password@replica-host:5432/thrillwiki
|
||||
|
||||
# ==============================================================================
|
||||
# Cache Configuration
|
||||
# ==============================================================================
|
||||
|
||||
# Redis URL for caching, sessions, and Celery broker
|
||||
# Format: redis://[:password@]host:port/db_number
|
||||
# PRODUCTION: Required - the application uses Redis for:
|
||||
# - Page and API response caching
|
||||
# - Session storage (faster than database sessions)
|
||||
# - Celery task queue broker
|
||||
# Without REDIS_URL in production, caching will fail and performance will degrade.
|
||||
REDIS_URL=redis://localhost:6379/1
|
||||
|
||||
# Optional: Separate Redis URLs for different cache purposes
|
||||
# REDIS_SESSIONS_URL=redis://localhost:6379/2
|
||||
# REDIS_API_URL=redis://localhost:6379/3
|
||||
|
||||
# Redis connection settings
|
||||
REDIS_MAX_CONNECTIONS=100
|
||||
REDIS_CONNECTION_TIMEOUT=20
|
||||
REDIS_IGNORE_EXCEPTIONS=True
|
||||
|
||||
# Cache middleware settings
|
||||
CACHE_MIDDLEWARE_SECONDS=300
|
||||
CACHE_MIDDLEWARE_KEY_PREFIX=thrillwiki
|
||||
CACHE_KEY_PREFIX=thrillwiki
|
||||
|
||||
# Local development cache URL (use for development without Redis)
|
||||
# CACHE_URL=locmem://
|
||||
|
||||
# ==============================================================================
|
||||
# Email Configuration
|
||||
# ==============================================================================
|
||||
|
||||
# Email backend
|
||||
# Options:
|
||||
# django.core.mail.backends.console.EmailBackend (development)
|
||||
# django_forwardemail.backends.ForwardEmailBackend (production with ForwardEmail)
|
||||
# django.core.mail.backends.smtp.EmailBackend (custom SMTP)
|
||||
EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend
|
||||
|
||||
# Server email address
|
||||
SERVER_EMAIL=django_webmaster@thrillwiki.com
|
||||
|
||||
# Default from email
|
||||
DEFAULT_FROM_EMAIL=ThrillWiki <noreply@thrillwiki.com>
|
||||
|
||||
# Email subject prefix for admin emails
|
||||
EMAIL_SUBJECT_PREFIX=[ThrillWiki]
|
||||
|
||||
# ForwardEmail configuration (for ForwardEmailBackend)
|
||||
FORWARD_EMAIL_BASE_URL=https://api.forwardemail.net
|
||||
FORWARD_EMAIL_API_KEY=your-forwardemail-api-key-here
|
||||
FORWARD_EMAIL_DOMAIN=your-domain.com
|
||||
|
||||
# SMTP configuration (for SMTPBackend)
|
||||
EMAIL_HOST=smtp.example.com
|
||||
EMAIL_PORT=587
|
||||
EMAIL_USE_TLS=True
|
||||
EMAIL_USE_SSL=False
|
||||
EMAIL_HOST_USER=your-email@example.com
|
||||
EMAIL_HOST_PASSWORD=your-app-password
|
||||
|
||||
# Email timeout in seconds
|
||||
EMAIL_TIMEOUT=30
|
||||
|
||||
# ==============================================================================
|
||||
# Security Settings
|
||||
# ==============================================================================
|
||||
|
||||
# Cloudflare Turnstile configuration (CAPTCHA alternative)
|
||||
# Get keys from: https://dash.cloudflare.com/?to=/:account/turnstile
|
||||
TURNSTILE_SITE_KEY=your-turnstile-site-key
|
||||
TURNSTILE_SECRET_KEY=your-turnstile-secret-key
|
||||
TURNSTILE_VERIFY_URL=https://challenges.cloudflare.com/turnstile/v0/siteverify
|
||||
|
||||
# SSL/HTTPS settings (enable all for production)
|
||||
SECURE_SSL_REDIRECT=False
|
||||
SESSION_COOKIE_SECURE=False
|
||||
CSRF_COOKIE_SECURE=False
|
||||
|
||||
# HSTS settings (HTTP Strict Transport Security)
|
||||
SECURE_HSTS_SECONDS=31536000
|
||||
SECURE_HSTS_INCLUDE_SUBDOMAINS=True
|
||||
SECURE_HSTS_PRELOAD=False
|
||||
|
||||
# Security headers
|
||||
SECURE_BROWSER_XSS_FILTER=True
|
||||
SECURE_CONTENT_TYPE_NOSNIFF=True
|
||||
X_FRAME_OPTIONS=DENY
|
||||
SECURE_REFERRER_POLICY=strict-origin-when-cross-origin
|
||||
SECURE_CROSS_ORIGIN_OPENER_POLICY=same-origin
|
||||
|
||||
# Session settings
|
||||
SESSION_COOKIE_AGE=3600
|
||||
SESSION_SAVE_EVERY_REQUEST=True
|
||||
SESSION_COOKIE_HTTPONLY=True
|
||||
SESSION_COOKIE_SAMESITE=Lax
|
||||
|
||||
# CSRF settings
|
||||
CSRF_COOKIE_HTTPONLY=True
|
||||
CSRF_COOKIE_SAMESITE=Lax
|
||||
|
||||
# Password minimum length
|
||||
PASSWORD_MIN_LENGTH=8
|
||||
|
||||
# ==============================================================================
|
||||
# GeoDjango Settings
|
||||
# ==============================================================================
|
||||
|
||||
# Library paths for GDAL and GEOS (required for GeoDjango)
|
||||
# macOS with Homebrew:
|
||||
GDAL_LIBRARY_PATH=/opt/homebrew/lib/libgdal.dylib
|
||||
GEOS_LIBRARY_PATH=/opt/homebrew/lib/libgeos_c.dylib
|
||||
|
||||
# Linux alternatives:
|
||||
# GDAL_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu/libgdal.so
|
||||
# GEOS_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu/libgeos_c.so
|
||||
|
||||
# ==============================================================================
|
||||
# API Configuration
|
||||
# ==============================================================================
|
||||
|
||||
# CORS settings
|
||||
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5174
|
||||
CORS_ALLOW_ALL_ORIGINS=False
|
||||
|
||||
# API rate limiting
|
||||
API_RATE_LIMIT_PER_MINUTE=60
|
||||
API_RATE_LIMIT_PER_HOUR=1000
|
||||
API_RATE_LIMIT_ANON_PER_MINUTE=60
|
||||
API_RATE_LIMIT_USER_PER_HOUR=1000
|
||||
|
||||
# API pagination
|
||||
API_PAGE_SIZE=20
|
||||
API_MAX_PAGE_SIZE=100
|
||||
API_VERSION=1.0.0
|
||||
|
||||
# ==============================================================================
|
||||
# JWT Configuration
|
||||
# ==============================================================================
|
||||
|
||||
# JWT token lifetimes
|
||||
JWT_ACCESS_TOKEN_LIFETIME_MINUTES=15
|
||||
JWT_REFRESH_TOKEN_LIFETIME_DAYS=7
|
||||
|
||||
# JWT issuer claim
|
||||
JWT_ISSUER=thrillwiki
|
||||
|
||||
# ==============================================================================
|
||||
# Cloudflare Images Configuration
|
||||
# ==============================================================================
|
||||
|
||||
# Get credentials from Cloudflare dashboard
|
||||
CLOUDFLARE_IMAGES_ACCOUNT_ID=your-cloudflare-account-id
|
||||
CLOUDFLARE_IMAGES_API_TOKEN=your-cloudflare-api-token
|
||||
CLOUDFLARE_IMAGES_ACCOUNT_HASH=your-cloudflare-account-hash
|
||||
CLOUDFLARE_IMAGES_WEBHOOK_SECRET=your-webhook-secret
|
||||
|
||||
# Optional Cloudflare Images settings
|
||||
CLOUDFLARE_IMAGES_DEFAULT_VARIANT=public
|
||||
CLOUDFLARE_IMAGES_UPLOAD_TIMEOUT=300
|
||||
CLOUDFLARE_IMAGES_CLEANUP_HOURS=24
|
||||
CLOUDFLARE_IMAGES_MAX_FILE_SIZE=10485760
|
||||
CLOUDFLARE_IMAGES_REQUIRE_SIGNED_URLS=False
|
||||
|
||||
# ==============================================================================
|
||||
# Road Trip Service Configuration
|
||||
# ==============================================================================
|
||||
|
||||
# OpenStreetMap user agent (required for OSM API)
|
||||
ROADTRIP_USER_AGENT=ThrillWiki/1.0 (https://thrillwiki.com)
|
||||
|
||||
# Cache timeouts
|
||||
ROADTRIP_CACHE_TIMEOUT=86400
|
||||
ROADTRIP_ROUTE_CACHE_TIMEOUT=21600
|
||||
|
||||
# Request settings
|
||||
ROADTRIP_MAX_REQUESTS_PER_SECOND=1
|
||||
ROADTRIP_REQUEST_TIMEOUT=10
|
||||
ROADTRIP_MAX_RETRIES=3
|
||||
ROADTRIP_BACKOFF_FACTOR=2
|
||||
|
||||
# ==============================================================================
|
||||
# Logging Configuration
|
||||
# ==============================================================================
|
||||
|
||||
# Log directory (relative to backend/)
|
||||
LOG_DIR=logs
|
||||
|
||||
# Log levels (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
ROOT_LOG_LEVEL=INFO
|
||||
DJANGO_LOG_LEVEL=WARNING
|
||||
DB_LOG_LEVEL=WARNING
|
||||
APP_LOG_LEVEL=INFO
|
||||
PERFORMANCE_LOG_LEVEL=INFO
|
||||
QUERY_LOG_LEVEL=WARNING
|
||||
NPLUSONE_LOG_LEVEL=WARNING
|
||||
REQUEST_LOG_LEVEL=INFO
|
||||
CELERY_LOG_LEVEL=INFO
|
||||
CONSOLE_LOG_LEVEL=INFO
|
||||
FILE_LOG_LEVEL=INFO
|
||||
|
||||
# Log formatters (verbose, json, simple)
|
||||
FILE_LOG_FORMATTER=json
|
||||
|
||||
# ==============================================================================
|
||||
# Monitoring & Errors
|
||||
# ==============================================================================
|
||||
|
||||
# Sentry configuration (optional, for error tracking)
|
||||
# SENTRY_DSN=https://your-sentry-dsn-here
|
||||
# SENTRY_ENVIRONMENT=development
|
||||
# SENTRY_TRACES_SAMPLE_RATE=0.1
|
||||
|
||||
# ==============================================================================
|
||||
# Feature Flags
|
||||
# ==============================================================================
|
||||
|
||||
# Development tools
|
||||
ENABLE_DEBUG_TOOLBAR=True
|
||||
ENABLE_SILK_PROFILER=False
|
||||
|
||||
# Django template support (can be disabled for API-only mode)
|
||||
TEMPLATES_ENABLED=True
|
||||
|
||||
# Autocomplete settings
|
||||
AUTOCOMPLETE_BLOCK_UNAUTHENTICATED=False
|
||||
|
||||
# ==============================================================================
|
||||
# Third-Party Configuration
|
||||
# ==============================================================================
|
||||
|
||||
# Frontend URL for email links and redirects
|
||||
FRONTEND_DOMAIN=https://thrillwiki.com
|
||||
|
||||
# Login/logout redirect URLs
|
||||
LOGIN_REDIRECT_URL=/
|
||||
ACCOUNT_LOGOUT_REDIRECT_URL=/
|
||||
|
||||
# Account settings
|
||||
ACCOUNT_EMAIL_VERIFICATION=mandatory
|
||||
|
||||
# ==============================================================================
|
||||
# File Upload Settings
|
||||
# ==============================================================================
|
||||
|
||||
# Maximum file size to upload into memory (bytes)
|
||||
FILE_UPLOAD_MAX_MEMORY_SIZE=2621440
|
||||
|
||||
# Maximum request data size (bytes)
|
||||
DATA_UPLOAD_MAX_MEMORY_SIZE=10485760
|
||||
|
||||
# Maximum number of GET/POST parameters
|
||||
DATA_UPLOAD_MAX_NUMBER_FIELDS=1000
|
||||
|
||||
# Static/Media URLs (usually don't need to change)
|
||||
STATIC_URL=static/
|
||||
MEDIA_URL=/media/
|
||||
|
||||
# WhiteNoise settings
|
||||
WHITENOISE_COMPRESSION_QUALITY=90
|
||||
WHITENOISE_MAX_AGE=31536000
|
||||
WHITENOISE_MANIFEST_STRICT=False
|
||||
|
||||
# ==============================================================================
|
||||
# Health Check Settings
|
||||
# ==============================================================================
|
||||
|
||||
# Disk usage threshold (percentage)
|
||||
HEALTH_CHECK_DISK_USAGE_MAX=90
|
||||
|
||||
# Minimum available memory (MB)
|
||||
HEALTH_CHECK_MEMORY_MIN=100
|
||||
|
||||
# ==============================================================================
|
||||
# Celery Configuration
|
||||
# ==============================================================================
|
||||
|
||||
# Celery task behavior (set to True for testing)
|
||||
CELERY_TASK_ALWAYS_EAGER=False
|
||||
CELERY_TASK_EAGER_PROPAGATES=False
|
||||
|
||||
# ==============================================================================
|
||||
# Debug Toolbar Configuration
|
||||
# ==============================================================================
|
||||
|
||||
# Internal IPs for debug toolbar (comma-separated)
|
||||
# INTERNAL_IPS=127.0.0.1,::1
|
||||
29
.flake8
Normal file
29
.flake8
Normal file
@@ -0,0 +1,29 @@
|
||||
[flake8]
|
||||
# Maximum line length (matches Black formatter)
|
||||
max-line-length = 88
|
||||
|
||||
# Exclude common directories that shouldn't be linted
|
||||
exclude =
|
||||
.git,
|
||||
__pycache__,
|
||||
.venv,
|
||||
venv,
|
||||
env,
|
||||
.env,
|
||||
migrations,
|
||||
node_modules,
|
||||
.tox,
|
||||
.mypy_cache,
|
||||
.pytest_cache,
|
||||
build,
|
||||
dist,
|
||||
*.egg-info
|
||||
|
||||
# Ignore line break style warnings which are style preferences
|
||||
# W503: line break before binary operator (conflicts with PEP8 W504)
|
||||
# W504: line break after binary operator (conflicts with PEP8 W503)
|
||||
# These warnings contradict each other, so it's best to ignore one or both
|
||||
ignore = W503,W504
|
||||
|
||||
# Maximum complexity for McCabe complexity checker
|
||||
max-complexity = 10
|
||||
83
.github/SECURITY.md
vendored
Normal file
83
.github/SECURITY.md
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| latest | :white_check_mark: |
|
||||
| < latest | :x: |
|
||||
|
||||
Only the latest version of ThrillWiki receives security updates.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
We take security vulnerabilities seriously. If you discover a security issue, please report it responsibly.
|
||||
|
||||
### How to Report
|
||||
|
||||
1. **Do not** create a public GitHub issue for security vulnerabilities
|
||||
2. Email your report to the project maintainers
|
||||
3. Include as much detail as possible:
|
||||
- Description of the vulnerability
|
||||
- Steps to reproduce
|
||||
- Potential impact
|
||||
- Affected versions
|
||||
- Any proof of concept (if available)
|
||||
|
||||
### What to Expect
|
||||
|
||||
- **Acknowledgment**: We will acknowledge receipt within 48 hours
|
||||
- **Assessment**: We will assess the vulnerability and its impact
|
||||
- **Updates**: We will keep you informed of our progress
|
||||
- **Resolution**: We aim to resolve critical vulnerabilities within 7 days
|
||||
- **Credit**: With your permission, we will credit you in our security advisories
|
||||
|
||||
### Scope
|
||||
|
||||
The following are in scope for security reports:
|
||||
|
||||
- ThrillWiki web application vulnerabilities
|
||||
- Authentication and authorization issues
|
||||
- Data exposure vulnerabilities
|
||||
- Injection vulnerabilities (SQL, XSS, etc.)
|
||||
- CSRF vulnerabilities
|
||||
- Server-side request forgery (SSRF)
|
||||
- Insecure direct object references
|
||||
|
||||
### Out of Scope
|
||||
|
||||
The following are out of scope:
|
||||
|
||||
- Denial of service attacks
|
||||
- Social engineering attacks
|
||||
- Physical security issues
|
||||
- Issues in third-party applications or services
|
||||
- Issues requiring physical access to a user's device
|
||||
- Vulnerabilities in outdated versions
|
||||
|
||||
## Security Measures
|
||||
|
||||
ThrillWiki implements the following security measures:
|
||||
|
||||
- HTTPS enforcement with HSTS
|
||||
- Content Security Policy
|
||||
- XSS protection with input sanitization
|
||||
- CSRF protection
|
||||
- SQL injection prevention via ORM
|
||||
- Rate limiting on authentication endpoints
|
||||
- Secure session management
|
||||
- JWT token rotation and blacklisting
|
||||
|
||||
For more details, see [docs/SECURITY.md](../docs/SECURITY.md).
|
||||
|
||||
## Security Updates
|
||||
|
||||
Security updates are released as soon as possible after a vulnerability is confirmed. We recommend:
|
||||
|
||||
1. Keep your installation up to date
|
||||
2. Subscribe to release notifications
|
||||
3. Review security advisories
|
||||
|
||||
## Contact
|
||||
|
||||
For security-related inquiries, please contact the project maintainers.
|
||||
22
.github/dependabot.yml
vendored
Normal file
22
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
version: 2
|
||||
updates:
|
||||
# Python dependencies
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
commit-message:
|
||||
prefix: "[DEPENDABOT] Update"
|
||||
open-pull-requests-limit: 10
|
||||
labels:
|
||||
- "dependencies"
|
||||
reviewers:
|
||||
- "pacnpal"
|
||||
|
||||
# GitHub Actions dependencies
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
commit-message:
|
||||
prefix: "[DEPENDABOT] Update Actions"
|
||||
54
.github/workflows/claude-code-review.yml
vendored
Normal file
54
.github/workflows/claude-code-review.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
name: Claude Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
# Optional: Only run on specific file changes
|
||||
# paths:
|
||||
# - "src/**/*.ts"
|
||||
# - "src/**/*.tsx"
|
||||
# - "src/**/*.js"
|
||||
# - "src/**/*.jsx"
|
||||
|
||||
jobs:
|
||||
claude-review:
|
||||
# Optional: Filter by PR author
|
||||
# if: |
|
||||
# github.event.pull_request.user.login == 'external-contributor' ||
|
||||
# github.event.pull_request.user.login == 'new-developer' ||
|
||||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code Review
|
||||
id: claude-review
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
prompt: |
|
||||
Please review this pull request and provide feedback on:
|
||||
- Code quality and best practices
|
||||
- Potential bugs or issues
|
||||
- Performance considerations
|
||||
- Security concerns
|
||||
- Test coverage
|
||||
|
||||
Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.
|
||||
|
||||
Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
|
||||
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options
|
||||
claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'
|
||||
|
||||
50
.github/workflows/claude.yml
vendored
Normal file
50
.github/workflows/claude.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
actions: read # Required for Claude to read CI results on PRs
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
|
||||
# This is an optional setting that allows Claude to read CI results on PRs
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
|
||||
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
|
||||
# prompt: 'Update the pull request description to include a summary of changes.'
|
||||
|
||||
# Optional: Add claude_args to customize behavior and configuration
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options
|
||||
# claude_args: '--model claude-opus-4-1-20250805 --allowed-tools Bash(gh pr:*)'
|
||||
|
||||
53
.github/workflows/dependency-update.yml
vendored
Normal file
53
.github/workflows/dependency-update.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Dependency Update Check
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 1' # Weekly on Monday at midnight UTC
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Install UV
|
||||
run: |
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Update Dependencies
|
||||
working-directory: backend
|
||||
run: |
|
||||
uv lock --upgrade
|
||||
uv sync
|
||||
|
||||
- name: Run Tests
|
||||
working-directory: backend
|
||||
run: |
|
||||
uv run manage.py test
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
commit-message: "chore: update dependencies"
|
||||
title: "chore: weekly dependency updates"
|
||||
body: |
|
||||
Automated dependency updates.
|
||||
|
||||
This PR was automatically generated by the dependency update workflow.
|
||||
|
||||
## Changes
|
||||
- Updated `uv.lock` with latest compatible versions
|
||||
|
||||
## Checklist
|
||||
- [ ] Review dependency changes
|
||||
- [ ] Verify all tests pass
|
||||
- [ ] Check for breaking changes
|
||||
branch: "dependency-updates"
|
||||
labels: dependencies
|
||||
96
.github/workflows/django.yml
vendored
Normal file
96
.github/workflows/django.yml
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
name: Django CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
python-version: ["3.13"]
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgis/postgis:16-3.4
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: test_thrillwiki
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
# Services only run on Linux runners
|
||||
if: runner.os == 'Linux'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Homebrew on Linux
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
echo "/home/linuxbrew/.linuxbrew/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Install GDAL with Homebrew
|
||||
run: brew install gdal
|
||||
|
||||
- name: Install PostGIS on macOS
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
brew install postgresql@16 postgis
|
||||
brew services start postgresql@16
|
||||
sleep 5
|
||||
/opt/homebrew/opt/postgresql@16/bin/createuser -s postgres || true
|
||||
/opt/homebrew/opt/postgresql@16/bin/createdb -U postgres test_thrillwiki || true
|
||||
/opt/homebrew/opt/postgresql@16/bin/psql -U postgres -d test_thrillwiki -c "CREATE EXTENSION IF NOT EXISTS postgis;" || true
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install UV
|
||||
run: |
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Cache UV dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/uv
|
||||
key: ${{ runner.os }}-uv-${{ hashFiles('backend/pyproject.toml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-uv-
|
||||
|
||||
- name: Install Dependencies
|
||||
working-directory: backend
|
||||
run: |
|
||||
uv sync --frozen
|
||||
|
||||
- name: Security Audit
|
||||
working-directory: backend
|
||||
run: |
|
||||
uv pip install pip-audit
|
||||
uv run pip-audit || true
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run Tests
|
||||
working-directory: backend
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: config.django.test
|
||||
TEST_DB_NAME: test_thrillwiki
|
||||
TEST_DB_USER: postgres
|
||||
TEST_DB_PASSWORD: postgres
|
||||
TEST_DB_HOST: localhost
|
||||
TEST_DB_PORT: 5432
|
||||
run: |
|
||||
uv run python manage.py test --settings=config.django.test --parallel
|
||||
34
.github/workflows/review.yml
vendored
Normal file
34
.github/workflows/review.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Claude Code Review
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
# Run on new/updated PRs
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
# Allow manual triggers for existing PRs
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'Pull Request Number'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
code-review:
|
||||
runs-on: ubuntu-latest
|
||||
environment: development_environment
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Run Claude Review
|
||||
uses: pacnpal/claude-code-review@main
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
anthropic-key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
pr-number: ${{ github.event.pull_request.number || inputs.pr_number }}
|
||||
171
.gitignore
vendored
171
.gitignore
vendored
@@ -1,34 +1,139 @@
|
||||
/.vscode
|
||||
/dev.sh
|
||||
/flake.nix
|
||||
venv
|
||||
/venv
|
||||
./venv
|
||||
venv/sour
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# Django
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
/backend/staticfiles/
|
||||
/backend/media/
|
||||
|
||||
# UV
|
||||
.uv/
|
||||
backend/.uv/
|
||||
|
||||
# Generated requirements files (auto-generated from pyproject.toml)
|
||||
# Uncomment if you want to track these files
|
||||
# backend/requirements.txt
|
||||
# backend/requirements-dev.txt
|
||||
# backend/requirements-test.txt
|
||||
|
||||
# Node.js
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-store/
|
||||
|
||||
# Vue.js / Vite
|
||||
/frontend/dist/
|
||||
/frontend/dist-ssr/
|
||||
*.local
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
backend/.env
|
||||
frontend/.env
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
accounts/__pycache__/
|
||||
__pycache__
|
||||
thrillwiki/__pycache__
|
||||
reviews/__pycache__
|
||||
parks/__pycache__
|
||||
media/__pycache__
|
||||
email_service/__pycache__
|
||||
core/__pycache__
|
||||
companies/__pycache__
|
||||
accounts/__pycache__
|
||||
venv
|
||||
accounts/__pycache__
|
||||
thrillwiki/__pycache__/settings.cpython-311.pyc
|
||||
accounts/migrations/__pycache__/__init__.cpython-311.pyc
|
||||
accounts/migrations/__pycache__/0001_initial.cpython-311.pyc
|
||||
companies/migrations/__pycache__
|
||||
moderation/__pycache__
|
||||
rides/__pycache__
|
||||
ssh_tools.jsonc
|
||||
thrillwiki/__pycache__/settings.cpython-312.pyc
|
||||
parks/__pycache__/views.cpython-312.pyc
|
||||
.venv/lib/python3.12/site-packages
|
||||
thrillwiki/__pycache__/urls.cpython-312.pyc
|
||||
thrillwiki/__pycache__/views.cpython-312.pyc
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Coverage
|
||||
coverage/
|
||||
*.lcov
|
||||
.nyc_output
|
||||
htmlcov/
|
||||
.coverage
|
||||
.coverage.*
|
||||
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
.cache
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# Build outputs
|
||||
/dist/
|
||||
/build/
|
||||
|
||||
# Backup files
|
||||
*.bak
|
||||
*.backup
|
||||
*.orig
|
||||
*.swp
|
||||
*_backup.*
|
||||
*_OLD_*
|
||||
|
||||
# Archive files
|
||||
*.tar.gz
|
||||
*.zip
|
||||
*.rar
|
||||
|
||||
# Security
|
||||
*.pem
|
||||
*.key
|
||||
*.cert
|
||||
|
||||
# Local development
|
||||
/uploads/
|
||||
/backups/
|
||||
.django_tailwind_cli/
|
||||
backend/.env
|
||||
frontend/.env
|
||||
|
||||
# Extracted packages
|
||||
django-forwardemail/
|
||||
frontend/
|
||||
frontend
|
||||
.snapshots
|
||||
web/next-env.d.ts
|
||||
web/.next/types/cache-life.d.ts
|
||||
.gitignore
|
||||
web/.next/types/routes.d.ts
|
||||
web/.next/types/validator.ts
|
||||
|
||||
251
.pylintrc
Normal file
251
.pylintrc
Normal file
@@ -0,0 +1,251 @@
|
||||
# =============================================================================
|
||||
# ThrillWiki Django Project - Pylint Configuration
|
||||
# =============================================================================
|
||||
#
|
||||
# Purpose: Django-aware Pylint configuration that suppresses false positives
|
||||
# while maintaining code quality standards.
|
||||
#
|
||||
# Alignment:
|
||||
# - Line length: 120 characters (matches Black and Ruff in pyproject.toml)
|
||||
# - Django version: 5.2.8
|
||||
#
|
||||
# Key Features:
|
||||
# - Suppresses false positives for Django ORM patterns (.objects, _meta, .DoesNotExist)
|
||||
# - Whitelists Django management command styling (self.style.SUCCESS, ERROR, etc.)
|
||||
# - Accommodates Django REST Framework patterns
|
||||
# - Allows django-fsm state machine patterns
|
||||
#
|
||||
# Maintenance:
|
||||
# - Review when upgrading Django or adding new dynamic attribute patterns
|
||||
# - Keep line-length aligned with Black/Ruff settings in pyproject.toml
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
[MASTER]
|
||||
# Use all available CPU cores for faster linting
|
||||
jobs=0
|
||||
|
||||
# Directories and files to exclude from linting
|
||||
ignore=.git,__pycache__,.venv,venv,migrations,node_modules,.tox,.pytest_cache,build,dist
|
||||
|
||||
# File patterns to ignore (e.g., Emacs backup files)
|
||||
ignore-patterns=^\.#
|
||||
|
||||
# Pickle collected data for faster subsequent runs
|
||||
persistent=yes
|
||||
|
||||
# =============================================================================
|
||||
# [MESSAGES CONTROL]
|
||||
# Disable checks that conflict with Django patterns and conventions
|
||||
# =============================================================================
|
||||
[MESSAGES CONTROL]
|
||||
disable=
|
||||
# C0114: missing-module-docstring
|
||||
# Django apps often don't need module docstrings; the app's purpose is
|
||||
# typically documented in apps.py or README
|
||||
C0114,
|
||||
|
||||
# C0115: missing-class-docstring
|
||||
# Django models, forms, and serializers are often self-documenting through
|
||||
# their field definitions and Meta classes
|
||||
C0115,
|
||||
|
||||
# C0116: missing-function-docstring
|
||||
# Allow simple functions and methods without docstrings; Django views and
|
||||
# model methods are often self-explanatory
|
||||
C0116,
|
||||
|
||||
# C0103: invalid-name
|
||||
# Django uses non-PEP8 names by convention (e.g., 'pk', 'id', 'qs')
|
||||
# and single-letter variables in comprehensions are acceptable
|
||||
C0103,
|
||||
|
||||
# C0411: wrong-import-order
|
||||
# Let isort/ruff handle import ordering; they have Django-specific rules
|
||||
C0411,
|
||||
|
||||
# C0415: import-outside-toplevel
|
||||
# Django often requires lazy imports to avoid circular dependencies,
|
||||
# especially in models.py and signals
|
||||
C0415,
|
||||
|
||||
# W0212: protected-access
|
||||
# Django extensively uses _meta for model introspection; this is documented
|
||||
# and supported API: https://docs.djangoproject.com/en/5.2/ref/models/meta/
|
||||
W0212,
|
||||
|
||||
# W0613: unused-argument
|
||||
# Django views, signals, and receivers often have unused parameters that
|
||||
# are required by the framework's signature (e.g., request, sender, **kwargs)
|
||||
W0613,
|
||||
|
||||
# R0903: too-few-public-methods
|
||||
# Django models, forms, and serializers can be simple data containers
|
||||
# with few or no methods beyond __str__
|
||||
R0903,
|
||||
|
||||
# R0801: duplicate-code
|
||||
# Django patterns naturally duplicate across apps (e.g., CRUD views,
|
||||
# model patterns); this is intentional for consistency
|
||||
R0801,
|
||||
|
||||
# E1101: no-member
|
||||
# Main source of false positives for Django's dynamic attributes:
|
||||
# - Model.objects (Manager)
|
||||
# - Model.DoesNotExist / MultipleObjectsReturned (exceptions)
|
||||
# - self.style.SUCCESS/ERROR (management commands)
|
||||
# - model._meta (Options)
|
||||
E1101
|
||||
|
||||
# =============================================================================
|
||||
# [TYPECHECK]
|
||||
# Whitelist Django's dynamically generated attributes
|
||||
# =============================================================================
|
||||
[TYPECHECK]
|
||||
# Django generates many attributes dynamically that Pylint cannot detect
|
||||
# statically. This list covers common patterns:
|
||||
#
|
||||
# - objects.* : Django ORM Manager methods (all, filter, get, create, etc.)
|
||||
# - DoesNotExist : Exception raised when Model.objects.get() finds nothing
|
||||
# - MultipleObjectsReturned : Exception when get() finds multiple objects
|
||||
# - _meta.* : Django model metadata API (fields, app_label, model_name)
|
||||
# - style.* : Django management command styling (SUCCESS, ERROR, WARNING, NOTICE)
|
||||
# - id, pk : Django auto-generated primary key fields
|
||||
# - REQUEST : Django request object attributes
|
||||
# - aq_* : Acquisition attributes (Zope/Plone compatibility)
|
||||
# - acl_users : Zope/Plone user folder
|
||||
#
|
||||
generated-members=
|
||||
REQUEST,
|
||||
acl_users,
|
||||
aq_parent,
|
||||
aq_inner,
|
||||
aq_explicit,
|
||||
aq_acquire,
|
||||
aq_base,
|
||||
objects,
|
||||
objects.*,
|
||||
DoesNotExist,
|
||||
MultipleObjectsReturned,
|
||||
_meta,
|
||||
_meta.*,
|
||||
style,
|
||||
style.*,
|
||||
id,
|
||||
pk
|
||||
|
||||
# =============================================================================
|
||||
# [FORMAT]
|
||||
# Code formatting settings - aligned with Black and Ruff (120 chars)
|
||||
# =============================================================================
|
||||
[FORMAT]
|
||||
# Maximum line length - matches Black and Ruff configuration in pyproject.toml
|
||||
max-line-length=120
|
||||
|
||||
# Use 4 spaces for indentation (Python standard)
|
||||
indent-string=' '
|
||||
|
||||
# Use Unix line endings (LF)
|
||||
expected-line-ending-format=LF
|
||||
|
||||
# =============================================================================
|
||||
# [BASIC]
|
||||
# Naming conventions and allowed short names
|
||||
# =============================================================================
|
||||
[BASIC]
|
||||
# Short variable names commonly used in Django and Python
|
||||
# - i, j, k : Loop counters
|
||||
# - ex : Exception variable
|
||||
# - Run : Django command method
|
||||
# - _ : Throwaway variable
|
||||
# - id, pk : Primary key (Django convention)
|
||||
# - qs : QuerySet abbreviation
|
||||
good-names=i,j,k,ex,Run,_,id,pk,qs
|
||||
|
||||
# Enforce snake_case for most identifiers (Python/Django convention)
|
||||
argument-naming-style=snake_case
|
||||
attr-naming-style=snake_case
|
||||
function-naming-style=snake_case
|
||||
method-naming-style=snake_case
|
||||
module-naming-style=snake_case
|
||||
variable-naming-style=snake_case
|
||||
|
||||
# PascalCase for classes
|
||||
class-naming-style=PascalCase
|
||||
|
||||
# UPPER_CASE for constants
|
||||
const-naming-style=UPPER_CASE
|
||||
|
||||
# =============================================================================
|
||||
# [DESIGN]
|
||||
# Complexity thresholds - relaxed for Django patterns
|
||||
# =============================================================================
|
||||
[DESIGN]
|
||||
# Django views and forms often need many arguments
|
||||
max-args=7
|
||||
|
||||
# Django models can have many fields
|
||||
max-attributes=12
|
||||
|
||||
# Allow complex boolean expressions
|
||||
max-bool-expr=5
|
||||
|
||||
# Django views can have complex branching logic
|
||||
max-branches=15
|
||||
|
||||
# Django views often have many local variables
|
||||
max-locals=20
|
||||
|
||||
# Django uses multiple inheritance (Model, Mixin classes)
|
||||
max-parents=7
|
||||
|
||||
# Django models and viewsets have many built-in methods
|
||||
max-public-methods=25
|
||||
|
||||
# Allow multiple return statements
|
||||
max-returns=6
|
||||
|
||||
# Django views can be lengthy
|
||||
max-statements=60
|
||||
|
||||
# Allow simple classes with no methods (e.g., Django Meta classes)
|
||||
min-public-methods=0
|
||||
|
||||
# =============================================================================
|
||||
# [SIMILARITIES]
|
||||
# Duplicate code detection settings
|
||||
# =============================================================================
|
||||
[SIMILARITIES]
|
||||
# Increase threshold to reduce false positives from Django boilerplate
|
||||
min-similarity-lines=6
|
||||
|
||||
# Don't flag similar comments
|
||||
ignore-comments=yes
|
||||
|
||||
# Don't flag similar docstrings
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Don't flag similar import blocks
|
||||
ignore-imports=yes
|
||||
|
||||
# =============================================================================
|
||||
# [VARIABLES]
|
||||
# Variable naming patterns
|
||||
# =============================================================================
|
||||
[VARIABLES]
|
||||
# Patterns for dummy/unused variables
|
||||
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
||||
|
||||
# Arguments that are commonly unused but required by framework signatures
|
||||
ignored-argument-names=_.*|^ignored_|^unused_|args|kwargs|request|pk
|
||||
|
||||
# =============================================================================
|
||||
# [IMPORTS]
|
||||
# Import checking settings
|
||||
# =============================================================================
|
||||
[IMPORTS]
|
||||
# Don't allow wildcard imports even with __all__ defined
|
||||
allow-wildcard-with-all=no
|
||||
|
||||
# Don't analyze fallback import blocks
|
||||
analyse-fallback-blocks=no
|
||||
18
.roo/mcp.json
Normal file
18
.roo/mcp.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"context7": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@upstash/context7-mcp"
|
||||
],
|
||||
"env": {
|
||||
"DEFAULT_MINIMUM_TOKENS": ""
|
||||
},
|
||||
"alwaysAllow": [
|
||||
"resolve-library-id",
|
||||
"get-library-docs"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
2
.roo/rules/api_architecture_enforcement
Normal file
2
.roo/rules/api_architecture_enforcement
Normal file
@@ -0,0 +1,2 @@
|
||||
## CRITICAL: Centralized API Structure
|
||||
All API endpoints MUST be centralized under the `backend/apps/api/v1/` structure. This is NON-NEGOTIABLE.
|
||||
49
.roo/rules/critical_rules
Normal file
49
.roo/rules/critical_rules
Normal file
@@ -0,0 +1,49 @@
|
||||
# Project Startup & Development Rules
|
||||
|
||||
## Server & Package Management
|
||||
- **Starting the Dev Server:** Always assume the server is running and changes have taken effect. If issues arise, run:
|
||||
```bash
|
||||
$PROJECT_ROOT/shared/scripts/start-servers.sh
|
||||
```
|
||||
- **Python Packages:** Only use UV to add packages:
|
||||
```bash
|
||||
cd $PROJECT_ROOT/backend && uv add <package>
|
||||
```
|
||||
- **Django Commands:** Always use `cd backend && uv run manage.py <command>` for all management tasks (migrations, shell, superuser, etc.). Never use `python manage.py` or `uv run python manage.py`.
|
||||
|
||||
## CRITICAL Frontend design rules
|
||||
- EVERYTHING must support both dark and light mode.
|
||||
- Make sure the light/dark mode toggle works with the Vue components and pages.
|
||||
- Leverage Tailwind CSS 4 and Shadcn UI components.
|
||||
|
||||
## Frontend API URL Rules
|
||||
- **Vite Proxy:** Always check `frontend/vite.config.ts` for proxy rules before changing frontend API URLs.
|
||||
- **URL Flow:** Understand how frontend URLs are rewritten by Vite proxy (e.g., `/api/auth/login/` → `/api/v1/auth/login/`).
|
||||
- **Verification:** Confirm proxy behavior via config and browser network tab. Only change URLs if proxy is NOT handling rewriting.
|
||||
- **Common Mistake:** Don’t assume frontend URLs are wrong due to proxy configuration.
|
||||
|
||||
## Entity Relationship Patterns
|
||||
- **Park:** Must have Operator (required), may have PropertyOwner (optional), cannot reference Company directly.
|
||||
- **Ride:** Must belong to Park, may have Manufacturer/Designer (optional), cannot reference Company directly.
|
||||
- **Entities:**
|
||||
- Operators: Operate parks.
|
||||
- PropertyOwners: Own park property (optional).
|
||||
- Manufacturers: Make rides.
|
||||
- Designers: Design rides.
|
||||
- All entities can have locations.
|
||||
- **Constraints:** Operator and PropertyOwner can be same or different. Manufacturers and Designers are distinct. Use proper foreign keys with correct null/blank settings.
|
||||
|
||||
## General Best Practices
|
||||
- Never assume blank output means success—always verify changes by testing.
|
||||
- Use context7 for documentation when troubleshooting.
|
||||
- Document changes with conport and reasoning.
|
||||
- Include relevant context and information in all changes.
|
||||
- Test and validate code before deployment.
|
||||
- Communicate changes clearly with your team.
|
||||
- Be open to feedback and continuous improvement.
|
||||
- Prioritize readability, maintainability, security, performance, scalability, and modularity.
|
||||
- Use meaningful names, DRY principles, clear comments, and handle errors gracefully.
|
||||
- Log important events/errors for troubleshooting.
|
||||
- Prefer existing modules/packages over new code.
|
||||
- Keep documentation up to date.
|
||||
- Consider security vulnerabilities and performance bottlenecks in all changes.
|
||||
390
.roo/rules/roo_code_conport_strategy
Normal file
390
.roo/rules/roo_code_conport_strategy
Normal file
@@ -0,0 +1,390 @@
|
||||
# --- ConPort Memory Strategy ---
|
||||
conport_memory_strategy:
|
||||
# CRITICAL: At the beginning of every session, the agent MUST execute the 'initialization' sequence
|
||||
# to determine the ConPort status and load relevant context.
|
||||
workspace_id_source: "The agent must obtain the absolute path to the current workspace to use as `workspace_id` for all ConPort tool calls. This might be available as `${workspaceFolder}` or require asking the user."
|
||||
|
||||
initialization:
|
||||
thinking_preamble: |
|
||||
|
||||
agent_action_plan:
|
||||
- step: 1
|
||||
action: "Determine `ACTUAL_WORKSPACE_ID`."
|
||||
- step: 2
|
||||
action: "Invoke `list_files` for `ACTUAL_WORKSPACE_ID + \"/context_portal/\"`."
|
||||
tool_to_use: "list_files"
|
||||
parameters: "path: ACTUAL_WORKSPACE_ID + \"/context_portal/\""
|
||||
- step: 3
|
||||
action: "Analyze result and branch based on 'context.db' existence."
|
||||
conditions:
|
||||
- if: "'context.db' is found"
|
||||
then_sequence: "load_existing_conport_context"
|
||||
- else: "'context.db' NOT found"
|
||||
then_sequence: "handle_new_conport_setup"
|
||||
|
||||
load_existing_conport_context:
|
||||
thinking_preamble: |
|
||||
|
||||
agent_action_plan:
|
||||
- step: 1
|
||||
description: "Attempt to load initial contexts from ConPort."
|
||||
actions:
|
||||
- "Invoke `get_product_context`... Store result."
|
||||
- "Invoke `get_active_context`... Store result."
|
||||
- "Invoke `get_decisions` (limit 5 for a better overview)... Store result."
|
||||
- "Invoke `get_progress` (limit 5)... Store result."
|
||||
- "Invoke `get_system_patterns` (limit 5)... Store result."
|
||||
- "Invoke `get_custom_data` (category: \"critical_settings\")... Store result."
|
||||
- "Invoke `get_custom_data` (category: \"ProjectGlossary\")... Store result."
|
||||
- "Invoke `get_recent_activity_summary` (default params, e.g., last 24h, limit 3 per type) for a quick catch-up. Store result."
|
||||
- step: 2
|
||||
description: "Analyze loaded context."
|
||||
conditions:
|
||||
- if: "results from step 1 are NOT empty/minimal"
|
||||
actions:
|
||||
- "Set internal status to [CONPORT_ACTIVE]."
|
||||
- "Inform user: \"ConPort memory initialized. Existing contexts and recent activity loaded.\""
|
||||
- "Use `ask_followup_question` with suggestions like \"Review recent activity?\", \"Continue previous task?\", \"What would you like to work on?\"."
|
||||
- else: "loaded context is empty/minimal despite DB file existing"
|
||||
actions:
|
||||
- "Set internal status to [CONPORT_ACTIVE]."
|
||||
- "Inform user: \"ConPort database file found, but it appears to be empty or minimally initialized. You can start by defining Product/Active Context or logging project information.\""
|
||||
- "Use `ask_followup_question` with suggestions like \"Define Product Context?\", \"Log a new decision?\"."
|
||||
- step: 3
|
||||
description: "Handle Load Failure (if step 1's `get_*` calls failed)."
|
||||
condition: "If any `get_*` calls in step 1 failed unexpectedly"
|
||||
action: "Fall back to `if_conport_unavailable_or_init_failed`."
|
||||
|
||||
handle_new_conport_setup:
|
||||
thinking_preamble: |
|
||||
|
||||
agent_action_plan:
|
||||
- step: 1
|
||||
action: "Inform user: \"No existing ConPort database found at `ACTUAL_WORKSPACE_ID + \"/context_portal/context.db\"`.\""
|
||||
- step: 2
|
||||
action: "Use `ask_followup_question`."
|
||||
tool_to_use: "ask_followup_question"
|
||||
parameters:
|
||||
question: "Would you like to initialize a new ConPort database for this workspace? The database will be created automatically when ConPort tools are first used."
|
||||
suggestions:
|
||||
- "Yes, initialize a new ConPort database."
|
||||
- "No, do not use ConPort for this session."
|
||||
- step: 3
|
||||
description: "Process user response."
|
||||
conditions:
|
||||
- if_user_response_is: "Yes, initialize a new ConPort database."
|
||||
actions:
|
||||
- "Inform user: \"Okay, a new ConPort database will be created.\""
|
||||
- description: "Attempt to bootstrap Product Context from projectBrief.md (this happens only on new setup)."
|
||||
thinking_preamble: |
|
||||
|
||||
sub_steps:
|
||||
- "Invoke `list_files` with `path: ACTUAL_WORKSPACE_ID` (non-recursive, just to check root)."
|
||||
- description: "Analyze `list_files` result for 'projectBrief.md'."
|
||||
conditions:
|
||||
- if: "'projectBrief.md' is found in the listing"
|
||||
actions:
|
||||
- "Invoke `read_file` for `ACTUAL_WORKSPACE_ID + \"/projectBrief.md\"`."
|
||||
- action: "Use `ask_followup_question`."
|
||||
tool_to_use: "ask_followup_question"
|
||||
parameters:
|
||||
question: "Found projectBrief.md in your workspace. As we're setting up ConPort for the first time, would you like to import its content into the Product Context?"
|
||||
suggestions:
|
||||
- "Yes, import its content now."
|
||||
- "No, skip importing it for now."
|
||||
- description: "Process user response to import projectBrief.md."
|
||||
conditions:
|
||||
- if_user_response_is: "Yes, import its content now."
|
||||
actions:
|
||||
- "(No need to `get_product_context` as DB is new and empty)"
|
||||
- "Prepare `content` for `update_product_context`. For example: `{\"initial_product_brief\": \"[content from projectBrief.md]\"}`."
|
||||
- "Invoke `update_product_context` with the prepared content."
|
||||
- "Inform user of the import result (success or failure)."
|
||||
- else: "'projectBrief.md' NOT found"
|
||||
actions:
|
||||
- action: "Use `ask_followup_question`."
|
||||
tool_to_use: "ask_followup_question"
|
||||
parameters:
|
||||
question: "`projectBrief.md` was not found in the workspace root. Would you like to define the initial Product Context manually now?"
|
||||
suggestions:
|
||||
- "Define Product Context manually."
|
||||
- "Skip for now."
|
||||
- "(If \"Define manually\", guide user through `update_product_context`)."
|
||||
- "Proceed to 'load_existing_conport_context' sequence (which will now load the potentially bootstrapped product context and other empty contexts)."
|
||||
- if_user_response_is: "No, do not use ConPort for this session."
|
||||
action: "Proceed to `if_conport_unavailable_or_init_failed` (with a message indicating user chose not to initialize)."
|
||||
|
||||
if_conport_unavailable_or_init_failed:
|
||||
thinking_preamble: |
|
||||
|
||||
agent_action: "Inform user: \"ConPort memory will not be used for this session. Status: [CONPORT_INACTIVE].\""
|
||||
|
||||
general:
|
||||
status_prefix: "Begin EVERY response with either '[CONPORT_ACTIVE]' or '[CONPORT_INACTIVE]'."
|
||||
proactive_logging_cue: "Remember to proactively identify opportunities to log or update ConPort based on the conversation (e.g., if user outlines a new plan, consider logging decisions or progress). Confirm with the user before logging."
|
||||
proactive_error_handling: "When encountering errors (e.g., tool failures, unexpected output), proactively log the error details using `log_custom_data` (category: 'ErrorLogs', key: 'timestamp_error_summary') and consider updating `active_context` with `open_issues` if it's a persistent problem. Prioritize using ConPort's `get_item_history` or `get_recent_activity_summary` to diagnose issues if they relate to past context changes."
|
||||
semantic_search_emphasis: "For complex or nuanced queries, especially when direct keyword search (`search_decisions_fts`, `search_custom_data_value_fts`) might be insufficient, prioritize using `semantic_search_conport` to leverage conceptual understanding and retrieve more relevant context. Explain to the user why semantic search is being used."
|
||||
|
||||
conport_updates:
|
||||
frequency: "UPDATE CONPORT THROUGHOUT THE CHAT SESSION, WHEN SIGNIFICANT CHANGES OCCUR, OR WHEN EXPLICITLY REQUESTED."
|
||||
workspace_id_note: "All ConPort tool calls require the `workspace_id`."
|
||||
tools:
|
||||
- name: get_product_context
|
||||
trigger: "To understand the overall project goals, features, or architecture at any time."
|
||||
action_description: |
|
||||
# Agent Action: Invoke `get_product_context` (`{"workspace_id": "..."}`). Result is a direct dictionary.
|
||||
- name: update_product_context
|
||||
trigger: "When the high-level project description, goals, features, or overall architecture changes significantly, as confirmed by the user."
|
||||
action_description: |
|
||||
<thinking>
|
||||
- Product context needs updating.
|
||||
- Step 1: (Optional but recommended if unsure of current state) Invoke `get_product_context`.
|
||||
- Step 2: Prepare the `content` (for full overwrite) or `patch_content` (partial update) dictionary.
|
||||
- To remove a key using `patch_content`, set its value to the special string sentinel `\"__DELETE__\"`.
|
||||
- Confirm changes with the user.
|
||||
</thinking>
|
||||
# Agent Action: Invoke `update_product_context` (`{"workspace_id": "...", "content": {...}}` or `{"workspace_id": "...", "patch_content": {"key_to_update": "new_value", "key_to_delete": "__DELETE__"}}`).
|
||||
- name: get_active_context
|
||||
trigger: "To understand the current task focus, immediate goals, or session-specific context."
|
||||
action_description: |
|
||||
# Agent Action: Invoke `get_active_context` (`{"workspace_id": "..."}`). Result is a direct dictionary.
|
||||
- name: update_active_context
|
||||
trigger: "When the current focus of work changes, new questions arise, or session-specific context needs updating (e.g., `current_focus`, `open_issues`), as confirmed by the user."
|
||||
action_description: |
|
||||
<thinking>
|
||||
- Active context needs updating.
|
||||
- Step 1: (Optional) Invoke `get_active_context` to retrieve the current state.
|
||||
- Step 2: Prepare `content` (for full overwrite) or `patch_content` (for partial update).
|
||||
- Common fields to update include `current_focus`, `open_issues`, and other session-specific data.
|
||||
- To remove a key using `patch_content`, set its value to the special string sentinel `\"__DELETE__\"`.
|
||||
- Confirm changes with the user.
|
||||
</thinking>
|
||||
# Agent Action: Invoke `update_active_context` (`{"workspace_id": "...", "content": {...}}` or `{"workspace_id": "...", "patch_content": {"current_focus": "new_focus", "open_issues": ["issue1", "issue2"], "key_to_delete": "__DELETE__"}}`).
|
||||
- name: log_decision
|
||||
trigger: "When a significant architectural or implementation decision is made and confirmed by the user."
|
||||
action_description: |
|
||||
# Agent Action: Invoke `log_decision` (`{"workspace_id": "...", "summary": "...", "rationale": "...", "tags": ["optional_tag"]}}`).
|
||||
- name: get_decisions
|
||||
trigger: "To retrieve a list of past decisions, e.g., to review history or find a specific decision."
|
||||
action_description: |
|
||||
# Agent Action: Invoke `get_decisions` (`{"workspace_id": "...", "limit": N, "tags_filter_include_all": ["tag1"], "tags_filter_include_any": ["tag2"]}}`). Explain optional filters.
|
||||
- name: search_decisions_fts
|
||||
trigger: "When searching for decisions by keywords in summary, rationale, details, or tags, and basic `get_decisions` is insufficient."
|
||||
action_description: |
|
||||
# Agent Action: Invoke `search_decisions_fts` (`{"workspace_id": "...", "query_term": "search keywords", "limit": N}}`).
|
||||
- name: delete_decision_by_id
|
||||
trigger: "When user explicitly confirms deletion of a specific decision by its ID."
|
||||
action_description: |
|
||||
# Agent Action: Invoke `delete_decision_by_id` (`{"workspace_id": "...", "decision_id": ID}}`). Emphasize prior confirmation.
|
||||
- name: log_progress
|
||||
trigger: "When a task begins, its status changes (e.g., TODO, IN_PROGRESS, DONE), or it's completed. Also when a new sub-task is defined."
|
||||
action_description: |
|
||||
# Agent Action: Invoke `log_progress` (`{"workspace_id": "...", "description": "...", "status": "...", "linked_item_type": "...", "linked_item_id": "..."}}`). Note: 'summary' was changed to 'description' for log_progress.
|
||||
- name: get_progress
|
||||
trigger: "To review current task statuses, find pending tasks, or check history of progress."
|
||||
action_description: |
|
||||
# Agent Action: Invoke `get_progress` (`{"workspace_id": "...", "status_filter": "...", "parent_id_filter": ID, "limit": N}}`).
|
||||
- name: update_progress
|
||||
trigger: "Updates an existing progress entry."
|
||||
action_description: |
|
||||
# Agent Action: Invoke `update_progress` (`{"workspace_id": "...", "progress_id": ID, "status": "...", "description": "...", "parent_id": ID}}`).
|
||||
- name: delete_progress_by_id
|
||||
trigger: "Deletes a progress entry by its ID."
|
||||
action_description: |
|
||||
# Agent Action: Invoke `delete_progress_by_id` (`{"workspace_id": "...", "progress_id": ID}}`).
|
||||
- name: log_system_pattern
|
||||
trigger: "When new architectural patterns are introduced, or existing ones are modified, as confirmed by the user."
|
||||
action_description: |
|
||||
# Agent Action: Invoke `log_system_pattern` (`{"workspace_id": "...", "name": "...", "description": "...", "tags": ["optional_tag"]}}`).
|
||||
- name: get_system_patterns
|
||||
trigger: "To retrieve a list of defined system patterns."
|
||||
action_description: |
|
||||
# Agent Action: Invoke `get_system_patterns` (`{"workspace_id": "...", "tags_filter_include_all": ["tag1"], "limit": N}}`). Note: limit was not in original example, added for consistency.
|
||||
- name: delete_system_pattern_by_id
|
||||
trigger: "When user explicitly confirms deletion of a specific system pattern by its ID."
|
||||
action_description: |
|
||||
# Agent Action: Invoke `delete_system_pattern_by_id` (`{"workspace_id": "...", "pattern_id": ID}}`). Emphasize prior confirmation.
|
||||
- name: log_custom_data
|
||||
trigger: "To store any other type of structured or unstructured project-related information not covered by other tools (e.g., glossary terms, technical specs, meeting notes), as confirmed by the user."
|
||||
action_description: |
|
||||
# Agent Action: Invoke `log_custom_data` (`{"workspace_id": "...", "category": "...", "key": "...", "value": {... or "string"}}`). Note: 'metadata' field is not part of log_custom_data args.
|
||||
- name: get_custom_data
|
||||
trigger: "To retrieve specific custom data by category and key."
|
||||
action_description: |
|
||||
# Agent Action: Invoke `get_custom_data` (`{"workspace_id": "...", "category": "...", "key": "..."}}`).
|
||||
- name: delete_custom_data
|
||||
trigger: "When user explicitly confirms deletion of specific custom data by category and key."
|
||||
action_description: |
|
||||
# Agent Action: Invoke `delete_custom_data` (`{"workspace_id": "...", "category": "...", "key": "..."}}`). Emphasize prior confirmation.
|
||||
- name: search_custom_data_value_fts
|
||||
trigger: "When searching for specific terms within any custom data values, categories, or keys."
|
||||
action_description: |
|
||||
# Agent Action: Invoke `search_custom_data_value_fts` (`{"workspace_id": "...", "query_term": "...", "category_filter": "...", "limit": N}}`).
|
||||
- name: search_project_glossary_fts
|
||||
trigger: "When specifically searching for terms within the 'ProjectGlossary' custom data category."
|
||||
action_description: |
|
||||
# Agent Action: Invoke `search_project_glossary_fts` (`{"workspace_id": "...", "query_term": "...", "limit": N}}`).
|
||||
- name: semantic_search_conport
|
||||
trigger: "When a natural language query requires conceptual understanding beyond keyword matching, or when direct keyword searches are insufficient."
|
||||
action_description: |
|
||||
# Agent Action: Invoke `semantic_search_conport` (`{"workspace_id": "...", "query_text": "...", "top_k": N, "filter_item_types": ["decision", "custom_data"]}}`). Explain filters.
|
||||
- name: link_conport_items
|
||||
trigger: "When a meaningful relationship is identified and confirmed between two existing ConPort items (e.g., a decision is implemented by a system pattern, a progress item tracks a decision)."
|
||||
action_description: |
|
||||
<thinking>
|
||||
- Need to link two items. Identify source type/ID, target type/ID, and relationship.
|
||||
- Common relationship_types: 'implements', 'related_to', 'tracks', 'blocks', 'clarifies', 'depends_on'. Propose a suitable one or ask user.
|
||||
</thinking>
|
||||
# Agent Action: Invoke `link_conport_items` (`{"workspace_id":"...", "source_item_type":"...", "source_item_id":"...", "target_item_type":"...", "target_item_id":"...", "relationship_type":"...", "description":"Optional notes"}`).
|
||||
- name: get_linked_items
|
||||
trigger: "To understand the relationships of a specific ConPort item, or to explore the knowledge graph around an item."
|
||||
action_description: |
|
||||
# Agent Action: Invoke `get_linked_items` (`{"workspace_id":"...", "item_type":"...", "item_id":"...", "relationship_type_filter":"...", "linked_item_type_filter":"...", "limit":N}`).
|
||||
- name: get_item_history
|
||||
trigger: "When needing to review past versions of Product Context or Active Context, or to see when specific changes were made."
|
||||
action_description: |
|
||||
# Agent Action: Invoke `get_item_history` (`{"workspace_id":"...", "item_type":"product_context" or "active_context", "limit":N, "version":V, "before_timestamp":"ISO_DATETIME", "after_timestamp":"ISO_DATETIME"}`).
|
||||
- name: batch_log_items
|
||||
trigger: "When the user provides a list of multiple items of the SAME type (e.g., several decisions, multiple new glossary terms) to be logged at once."
|
||||
action_description: |
|
||||
<thinking>
|
||||
- User provided multiple items. Verify they are of the same loggable type.
|
||||
- Construct the `items` list, where each element is a dictionary of arguments for the single-item log tool (e.g., for `log_decision`).
|
||||
</thinking>
|
||||
# Agent Action: Invoke `batch_log_items` (`{"workspace_id":"...", "item_type":"decision", "items": [{"summary":"...", "rationale":"..."}, {"summary":"..."}] }`).
|
||||
- name: get_recent_activity_summary
|
||||
trigger: "At the start of a new session to catch up, or when the user asks for a summary of recent project activities."
|
||||
action_description: |
|
||||
# Agent Action: Invoke `get_recent_activity_summary` (`{"workspace_id":"...", "hours_ago":H, "since_timestamp":"ISO_DATETIME", "limit_per_type":N}`). Explain default if no time args.
|
||||
- name: get_conport_schema
|
||||
trigger: "If there's uncertainty about available ConPort tools or their arguments during a session (internal LLM check), or if an advanced user specifically asks for the server's tool schema."
|
||||
action_description: |
|
||||
# Agent Action: Invoke `get_conport_schema` (`{"workspace_id":"..."}`). Primarily for internal LLM reference or direct user request.
|
||||
- name: export_conport_to_markdown
|
||||
trigger: "When the user requests to export the current ConPort data to markdown files (e.g., for backup, sharing, or version control)."
|
||||
action_description: |
|
||||
# Agent Action: Invoke `export_conport_to_markdown` (`{"workspace_id":"...", "output_path":"optional/relative/path"}`). Explain default output path if not provided.
|
||||
- name: import_markdown_to_conport
|
||||
trigger: "When the user requests to import ConPort data from a directory of markdown files previously exported by this system."
|
||||
action_description: |
|
||||
# Agent Action: Invoke `import_markdown_to_conport` (`{"workspace_id":"...", "input_path":"optional/relative/path"}`). Explain default input path. Warn about potential overwrites or merges if data already exists.
|
||||
- name: reconfigure_core_guidance
|
||||
type: guidance
|
||||
product_active_context: "The internal JSON structure of 'Product Context' and 'Active Context' (the `content` field) is flexible. Work with the user to define and evolve this structure via `update_product_context` and `update_active_context`. The server stores this `content` as a JSON blob."
|
||||
decisions_progress_patterns: "The fundamental fields for Decisions, Progress, and System Patterns are fixed by ConPort's tools. For significantly different structures or additional fields, guide the user to create a new custom context category using `log_custom_data` (e.g., category: 'project_milestones_detailed')."
|
||||
|
||||
conport_sync_routine:
|
||||
trigger: "^(Sync ConPort|ConPort Sync)$"
|
||||
user_acknowledgement_text: "[CONPORT_SYNCING]"
|
||||
instructions:
|
||||
- "Halt Current Task: Stop current activity."
|
||||
- "Acknowledge Command: Send `[CONPORT_SYNCING]` to the user."
|
||||
- "Review Chat History: Analyze the complete current chat session for new information, decisions, progress, context changes, clarifications, and potential new relationships between items."
|
||||
core_update_process:
|
||||
thinking_preamble: |
|
||||
- Synchronize ConPort with information from the current chat session.
|
||||
- Use appropriate ConPort tools based on identified changes.
|
||||
- For `update_product_context` and `update_active_context`, first fetch current content, then merge/update (potentially using `patch_content`), then call the update tool with the *complete new content object* or the patch.
|
||||
- All tool calls require the `workspace_id`.
|
||||
agent_action_plan_illustrative:
|
||||
- "Log new decisions (use `log_decision`)."
|
||||
- "Log task progress/status changes (use `log_progress`)."
|
||||
- "Update existing progress entries (use `update_progress`)."
|
||||
- "Delete progress entries (use `delete_progress_by_id`)."
|
||||
- "Log new system patterns (use `log_system_pattern`)."
|
||||
- "Update Active Context (use `get_active_context` then `update_active_context` with full or patch)."
|
||||
- "Update Product Context if significant changes (use `get_product_context` then `update_product_context` with full or patch)."
|
||||
- "Log new custom context, including ProjectGlossary terms (use `log_custom_data`)."
|
||||
- "Identify and log new relationships between items (use `link_conport_items`)."
|
||||
- "If many items of the same type were discussed, consider `batch_log_items`."
|
||||
- "After updates, consider a brief `get_recent_activity_summary` to confirm and refresh understanding."
|
||||
post_sync_actions:
|
||||
- "Inform user: ConPort synchronized with session info."
|
||||
- "Resume previous task or await new instructions."
|
||||
|
||||
dynamic_context_retrieval_for_rag:
|
||||
description: |
|
||||
Guidance for dynamically retrieving and assembling context from ConPort to answer user queries or perform tasks,
|
||||
enhancing Retrieval Augmented Generation (RAG) capabilities.
|
||||
trigger: "When the AI needs to answer a specific question, perform a task requiring detailed project knowledge, or generate content based on ConPort data."
|
||||
goal: "To construct a concise, highly relevant context set for the LLM, improving the accuracy and relevance of its responses."
|
||||
steps:
|
||||
- step: 1
|
||||
action: "Analyze User Query/Task"
|
||||
details: "Deconstruct the user's request to identify key entities, concepts, keywords, and the specific type of information needed from ConPort."
|
||||
- step: 2
|
||||
action: "Prioritized Retrieval Strategy"
|
||||
details: |
|
||||
Based on the analysis, select the most appropriate ConPort tools:
|
||||
- **Targeted FTS:** Use `search_decisions_fts`, `search_custom_data_value_fts`, `search_project_glossary_fts` for keyword-based searches if specific terms are evident.
|
||||
- **Specific Item Retrieval:** Use `get_custom_data` (if category/key known), `get_decisions` (by ID or for recent items), `get_system_patterns`, `get_progress` if the query points to specific item types or IDs.
|
||||
- **(Future):** Prioritize semantic search tools once available for conceptual queries.
|
||||
- **Broad Context (Fallback):** Use `get_product_context` or `get_active_context` as a fallback if targeted retrieval yields little, but be mindful of their size.
|
||||
- step: 3
|
||||
action: "Retrieve Initial Set"
|
||||
details: "Execute the chosen tool(s) to retrieve an initial, small set (e.g., top 3-5) of the most relevant items or data snippets."
|
||||
- step: 4
|
||||
action: "Contextual Expansion (Optional)"
|
||||
details: "For the most promising items from Step 3, consider using `get_linked_items` to fetch directly related items (1-hop). This can provide crucial context or disambiguation. Use judiciously to avoid excessive data."
|
||||
- step: 5
|
||||
action: "Synthesize and Filter"
|
||||
details: |
|
||||
Review the retrieved information (initial set + expanded context).
|
||||
- **Filter:** Discard irrelevant items or parts of items.
|
||||
- **Synthesize/Summarize:** If multiple relevant pieces of information are found, synthesize them into a concise summary that directly addresses the query/task. Extract only the most pertinent sentences or facts.
|
||||
- step: 6
|
||||
action: "Assemble Prompt Context"
|
||||
details: |
|
||||
Construct the context portion of the LLM prompt using the filtered and synthesized information.
|
||||
- **Clarity:** Clearly delineate this retrieved context from the user's query or other parts of the prompt.
|
||||
- **Attribution (Optional but Recommended):** If possible, briefly note the source of the information (e.g., "From Decision D-42:", "According to System Pattern SP-5:").
|
||||
- **Brevity:** Strive for relevance and conciseness. Avoid including large, unprocessed chunks of data unless absolutely necessary and directly requested.
|
||||
general_principles:
|
||||
- "Prefer targeted retrieval over broad context dumps."
|
||||
- "Iterate if initial retrieval is insufficient: try different keywords or tools."
|
||||
- "Balance context richness with prompt token limits."
|
||||
|
||||
proactive_knowledge_graph_linking:
|
||||
description: |
|
||||
Guidance for the AI to proactively identify and suggest the creation of links between ConPort items,
|
||||
enriching the project's knowledge graph based on conversational context.
|
||||
trigger: "During ongoing conversation, when the AI observes potential relationships (e.g., causal, implementational, clarifying) between two or more discussed ConPort items or concepts that are likely represented as ConPort items."
|
||||
goal: "To actively build and maintain a rich, interconnected knowledge graph within ConPort by capturing relationships that might otherwise be missed."
|
||||
steps:
|
||||
- step: 1
|
||||
action: "Monitor Conversational Context"
|
||||
details: "Continuously analyze the user's statements and the flow of discussion for mentions of ConPort items (explicitly by ID, or implicitly by well-known names/summaries) and the relationships being described or implied between them."
|
||||
- step: 2
|
||||
action: "Identify Potential Links"
|
||||
details: |
|
||||
Look for patterns such as:
|
||||
- User states "Decision X led to us doing Y (which is Progress item P-3)."
|
||||
- User discusses how System Pattern SP-2 helps address a concern noted in Decision D-5.
|
||||
- User outlines a task (Progress P-10) that implements a specific feature detailed in a `custom_data` spec (CD-Spec-FeatureX).
|
||||
- step: 3
|
||||
action: "Formulate and Propose Link Suggestion"
|
||||
details: |
|
||||
If a potential link is identified:
|
||||
- Clearly state the items involved (e.g., "Decision D-5", "System Pattern SP-2").
|
||||
- Describe the perceived relationship (e.g., "It seems SP-2 addresses a concern in D-5.").
|
||||
- Propose creating a link using `ask_followup_question`.
|
||||
- Example Question: "I noticed we're discussing Decision D-5 and System Pattern SP-2. It sounds like SP-2 might 'address_concern_in' D-5. Would you like me to create this link in ConPort? You can also suggest a different relationship type."
|
||||
- Suggested Answers:
|
||||
- "Yes, link them with 'addresses_concern_in'."
|
||||
- "Yes, but use relationship type: [user types here]."
|
||||
- "No, don't link them now."
|
||||
- Offer common relationship types as examples if needed: 'implements', 'clarifies', 'related_to', 'depends_on', 'blocks', 'resolves', 'derived_from'.
|
||||
- step: 4
|
||||
action: "Gather Details and Execute Linking"
|
||||
details: |
|
||||
If the user confirms:
|
||||
- Ensure you have the correct source item type, source item ID, target item type, target item ID, and the agreed-upon relationship type.
|
||||
- Ask for an optional brief description for the link if the relationship isn't obvious.
|
||||
- Invoke the `link_conport_items` tool.
|
||||
- step: 5
|
||||
action: "Confirm Outcome"
|
||||
details: "Inform the user of the success or failure of the `link_conport_items` tool call."
|
||||
general_principles:
|
||||
- "Be helpful, not intrusive. If the user declines a suggestion, accept and move on."
|
||||
- "Prioritize clear, strong relationships over tenuous ones."
|
||||
- "This strategy complements the general `proactive_logging_cue` by providing specific guidance for link creation."
|
||||
35
.roomodes
Normal file
35
.roomodes
Normal file
@@ -0,0 +1,35 @@
|
||||
customModes:
|
||||
- slug: project-research
|
||||
name: 🔍 Project Research
|
||||
roleDefinition: |
|
||||
You are a detailed-oriented research assistant specializing in examining and understanding codebases. Your primary responsibility is to analyze the file structure, content, and dependencies of a given project to provide comprehensive context relevant to specific user queries.
|
||||
whenToUse: |
|
||||
Use this mode when you need to thoroughly investigate and understand a codebase structure, analyze project architecture, or gather comprehensive context about existing implementations. Ideal for onboarding to new projects, understanding complex codebases, or researching how specific features are implemented across the project.
|
||||
description: Investigate and analyze codebase structure
|
||||
groups:
|
||||
- read
|
||||
source: project
|
||||
customInstructions: |
|
||||
Your role is to deeply investigate and summarize the structure and implementation details of the project codebase. To achieve this effectively, you must:
|
||||
|
||||
1. Start by carefully examining the file structure of the entire project, with a particular emphasis on files located within the "docs" folder. These files typically contain crucial context, architectural explanations, and usage guidelines.
|
||||
|
||||
2. When given a specific query, systematically identify and gather all relevant context from:
|
||||
- Documentation files in the "docs" folder that provide background information, specifications, or architectural insights.
|
||||
- Relevant type definitions and interfaces, explicitly citing their exact location (file path and line number) within the source code.
|
||||
- Implementations directly related to the query, clearly noting their file locations and providing concise yet comprehensive summaries of how they function.
|
||||
- Important dependencies, libraries, or modules involved in the implementation, including their usage context and significance to the query.
|
||||
|
||||
3. Deliver a structured, detailed report that clearly outlines:
|
||||
- An overview of relevant documentation insights.
|
||||
- Specific type definitions and their exact locations.
|
||||
- Relevant implementations, including file paths, functions or methods involved, and a brief explanation of their roles.
|
||||
- Critical dependencies and their roles in relation to the query.
|
||||
|
||||
4. Always cite precise file paths, function names, and line numbers to enhance clarity and ease of navigation.
|
||||
|
||||
5. Organize your findings in logical sections, making it straightforward for the user to understand the project's structure and implementation status relevant to their request.
|
||||
|
||||
6. Ensure your response directly addresses the user's query and helps them fully grasp the relevant aspects of the project's current state.
|
||||
|
||||
These specific instructions supersede any conflicting general instructions you might otherwise follow. Your detailed report should enable effective decision-making and next steps within the overall workflow.
|
||||
@@ -1,247 +0,0 @@
|
||||
<#
|
||||
.Synopsis
|
||||
Activate a Python virtual environment for the current PowerShell session.
|
||||
|
||||
.Description
|
||||
Pushes the python executable for a virtual environment to the front of the
|
||||
$Env:PATH environment variable and sets the prompt to signify that you are
|
||||
in a Python virtual environment. Makes use of the command line switches as
|
||||
well as the `pyvenv.cfg` file values present in the virtual environment.
|
||||
|
||||
.Parameter VenvDir
|
||||
Path to the directory that contains the virtual environment to activate. The
|
||||
default value for this is the parent of the directory that the Activate.ps1
|
||||
script is located within.
|
||||
|
||||
.Parameter Prompt
|
||||
The prompt prefix to display when this virtual environment is activated. By
|
||||
default, this prompt is the name of the virtual environment folder (VenvDir)
|
||||
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
|
||||
|
||||
.Example
|
||||
Activate.ps1
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -Verbose
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||
and shows extra information about the activation as it executes.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
|
||||
Activates the Python virtual environment located in the specified location.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -Prompt "MyPython"
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||
and prefixes the current prompt with the specified string (surrounded in
|
||||
parentheses) while the virtual environment is active.
|
||||
|
||||
.Notes
|
||||
On Windows, it may be required to enable this Activate.ps1 script by setting the
|
||||
execution policy for the user. You can do this by issuing the following PowerShell
|
||||
command:
|
||||
|
||||
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
|
||||
For more information on Execution Policies:
|
||||
https://go.microsoft.com/fwlink/?LinkID=135170
|
||||
|
||||
#>
|
||||
Param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[String]
|
||||
$VenvDir,
|
||||
[Parameter(Mandatory = $false)]
|
||||
[String]
|
||||
$Prompt
|
||||
)
|
||||
|
||||
<# Function declarations --------------------------------------------------- #>
|
||||
|
||||
<#
|
||||
.Synopsis
|
||||
Remove all shell session elements added by the Activate script, including the
|
||||
addition of the virtual environment's Python executable from the beginning of
|
||||
the PATH variable.
|
||||
|
||||
.Parameter NonDestructive
|
||||
If present, do not remove this function from the global namespace for the
|
||||
session.
|
||||
|
||||
#>
|
||||
function global:deactivate ([switch]$NonDestructive) {
|
||||
# Revert to original values
|
||||
|
||||
# The prior prompt:
|
||||
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
|
||||
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
|
||||
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
|
||||
}
|
||||
|
||||
# The prior PYTHONHOME:
|
||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
|
||||
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
|
||||
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
|
||||
}
|
||||
|
||||
# The prior PATH:
|
||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
|
||||
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
|
||||
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
|
||||
}
|
||||
|
||||
# Just remove the VIRTUAL_ENV altogether:
|
||||
if (Test-Path -Path Env:VIRTUAL_ENV) {
|
||||
Remove-Item -Path env:VIRTUAL_ENV
|
||||
}
|
||||
|
||||
# Just remove VIRTUAL_ENV_PROMPT altogether.
|
||||
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
|
||||
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
|
||||
}
|
||||
|
||||
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
|
||||
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
|
||||
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
|
||||
}
|
||||
|
||||
# Leave deactivate function in the global namespace if requested:
|
||||
if (-not $NonDestructive) {
|
||||
Remove-Item -Path function:deactivate
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.Description
|
||||
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
|
||||
given folder, and returns them in a map.
|
||||
|
||||
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
|
||||
two strings separated by `=` (with any amount of whitespace surrounding the =)
|
||||
then it is considered a `key = value` line. The left hand string is the key,
|
||||
the right hand is the value.
|
||||
|
||||
If the value starts with a `'` or a `"` then the first and last character is
|
||||
stripped from the value before being captured.
|
||||
|
||||
.Parameter ConfigDir
|
||||
Path to the directory that contains the `pyvenv.cfg` file.
|
||||
#>
|
||||
function Get-PyVenvConfig(
|
||||
[String]
|
||||
$ConfigDir
|
||||
) {
|
||||
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
|
||||
|
||||
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
|
||||
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
|
||||
|
||||
# An empty map will be returned if no config file is found.
|
||||
$pyvenvConfig = @{ }
|
||||
|
||||
if ($pyvenvConfigPath) {
|
||||
|
||||
Write-Verbose "File exists, parse `key = value` lines"
|
||||
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
|
||||
|
||||
$pyvenvConfigContent | ForEach-Object {
|
||||
$keyval = $PSItem -split "\s*=\s*", 2
|
||||
if ($keyval[0] -and $keyval[1]) {
|
||||
$val = $keyval[1]
|
||||
|
||||
# Remove extraneous quotations around a string value.
|
||||
if ("'""".Contains($val.Substring(0, 1))) {
|
||||
$val = $val.Substring(1, $val.Length - 2)
|
||||
}
|
||||
|
||||
$pyvenvConfig[$keyval[0]] = $val
|
||||
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
|
||||
}
|
||||
}
|
||||
}
|
||||
return $pyvenvConfig
|
||||
}
|
||||
|
||||
|
||||
<# Begin Activate script --------------------------------------------------- #>
|
||||
|
||||
# Determine the containing directory of this script
|
||||
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
$VenvExecDir = Get-Item -Path $VenvExecPath
|
||||
|
||||
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
|
||||
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
|
||||
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
|
||||
|
||||
# Set values required in priority: CmdLine, ConfigFile, Default
|
||||
# First, get the location of the virtual environment, it might not be
|
||||
# VenvExecDir if specified on the command line.
|
||||
if ($VenvDir) {
|
||||
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
|
||||
}
|
||||
else {
|
||||
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
|
||||
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
|
||||
Write-Verbose "VenvDir=$VenvDir"
|
||||
}
|
||||
|
||||
# Next, read the `pyvenv.cfg` file to determine any required value such
|
||||
# as `prompt`.
|
||||
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
|
||||
|
||||
# Next, set the prompt from the command line, or the config file, or
|
||||
# just use the name of the virtual environment folder.
|
||||
if ($Prompt) {
|
||||
Write-Verbose "Prompt specified as argument, using '$Prompt'"
|
||||
}
|
||||
else {
|
||||
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
|
||||
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
|
||||
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
|
||||
$Prompt = $pyvenvCfg['prompt'];
|
||||
}
|
||||
else {
|
||||
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
|
||||
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
|
||||
$Prompt = Split-Path -Path $venvDir -Leaf
|
||||
}
|
||||
}
|
||||
|
||||
Write-Verbose "Prompt = '$Prompt'"
|
||||
Write-Verbose "VenvDir='$VenvDir'"
|
||||
|
||||
# Deactivate any currently active virtual environment, but leave the
|
||||
# deactivate function in place.
|
||||
deactivate -nondestructive
|
||||
|
||||
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
|
||||
# that there is an activated venv.
|
||||
$env:VIRTUAL_ENV = $VenvDir
|
||||
|
||||
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
|
||||
|
||||
Write-Verbose "Setting prompt to '$Prompt'"
|
||||
|
||||
# Set the prompt to include the env name
|
||||
# Make sure _OLD_VIRTUAL_PROMPT is global
|
||||
function global:_OLD_VIRTUAL_PROMPT { "" }
|
||||
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
|
||||
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
|
||||
|
||||
function global:prompt {
|
||||
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
|
||||
_OLD_VIRTUAL_PROMPT
|
||||
}
|
||||
$env:VIRTUAL_ENV_PROMPT = $Prompt
|
||||
}
|
||||
|
||||
# Clear PYTHONHOME
|
||||
if (Test-Path -Path Env:PYTHONHOME) {
|
||||
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
|
||||
Remove-Item -Path Env:PYTHONHOME
|
||||
}
|
||||
|
||||
# Add the venv to the PATH
|
||||
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
|
||||
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
|
||||
@@ -1,70 +0,0 @@
|
||||
# This file must be used with "source bin/activate" *from bash*
|
||||
# You cannot run it directly
|
||||
|
||||
deactivate () {
|
||||
# reset old environment variables
|
||||
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
|
||||
PATH="${_OLD_VIRTUAL_PATH:-}"
|
||||
export PATH
|
||||
unset _OLD_VIRTUAL_PATH
|
||||
fi
|
||||
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
|
||||
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
|
||||
export PYTHONHOME
|
||||
unset _OLD_VIRTUAL_PYTHONHOME
|
||||
fi
|
||||
|
||||
# Call hash to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
hash -r 2> /dev/null
|
||||
|
||||
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
||||
PS1="${_OLD_VIRTUAL_PS1:-}"
|
||||
export PS1
|
||||
unset _OLD_VIRTUAL_PS1
|
||||
fi
|
||||
|
||||
unset VIRTUAL_ENV
|
||||
unset VIRTUAL_ENV_PROMPT
|
||||
if [ ! "${1:-}" = "nondestructive" ] ; then
|
||||
# Self destruct!
|
||||
unset -f deactivate
|
||||
fi
|
||||
}
|
||||
|
||||
# unset irrelevant variables
|
||||
deactivate nondestructive
|
||||
|
||||
# on Windows, a path can contain colons and backslashes and has to be converted:
|
||||
if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then
|
||||
# transform D:\path\to\venv to /d/path/to/venv on MSYS
|
||||
# and to /cygdrive/d/path/to/venv on Cygwin
|
||||
export VIRTUAL_ENV=$(cygpath "/home/pac/thrillwiki/thrillwiki_django_no_react/.venv")
|
||||
else
|
||||
# use the path as-is
|
||||
export VIRTUAL_ENV="/home/pac/thrillwiki/thrillwiki_django_no_react/.venv"
|
||||
fi
|
||||
|
||||
_OLD_VIRTUAL_PATH="$PATH"
|
||||
PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
export PATH
|
||||
|
||||
# unset PYTHONHOME if set
|
||||
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
|
||||
# could use `if (set -u; : $PYTHONHOME) ;` in bash
|
||||
if [ -n "${PYTHONHOME:-}" ] ; then
|
||||
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
|
||||
unset PYTHONHOME
|
||||
fi
|
||||
|
||||
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
||||
_OLD_VIRTUAL_PS1="${PS1:-}"
|
||||
PS1="(.venv) ${PS1:-}"
|
||||
export PS1
|
||||
VIRTUAL_ENV_PROMPT="(.venv) "
|
||||
export VIRTUAL_ENV_PROMPT
|
||||
fi
|
||||
|
||||
# Call hash to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
hash -r 2> /dev/null
|
||||
@@ -1,27 +0,0 @@
|
||||
# This file must be used with "source bin/activate.csh" *from csh*.
|
||||
# You cannot run it directly.
|
||||
|
||||
# Created by Davide Di Blasi <davidedb@gmail.com>.
|
||||
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
|
||||
|
||||
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
|
||||
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
setenv VIRTUAL_ENV "/home/pac/thrillwiki/thrillwiki_django_no_react/.venv"
|
||||
|
||||
set _OLD_VIRTUAL_PATH="$PATH"
|
||||
setenv PATH "$VIRTUAL_ENV/bin:$PATH"
|
||||
|
||||
|
||||
set _OLD_VIRTUAL_PROMPT="$prompt"
|
||||
|
||||
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
|
||||
set prompt = "(.venv) $prompt"
|
||||
setenv VIRTUAL_ENV_PROMPT "(.venv) "
|
||||
endif
|
||||
|
||||
alias pydoc python -m pydoc
|
||||
|
||||
rehash
|
||||
@@ -1,69 +0,0 @@
|
||||
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
|
||||
# (https://fishshell.com/). You cannot run it directly.
|
||||
|
||||
function deactivate -d "Exit virtual environment and return to normal shell environment"
|
||||
# reset old environment variables
|
||||
if test -n "$_OLD_VIRTUAL_PATH"
|
||||
set -gx PATH $_OLD_VIRTUAL_PATH
|
||||
set -e _OLD_VIRTUAL_PATH
|
||||
end
|
||||
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
|
||||
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
|
||||
set -e _OLD_VIRTUAL_PYTHONHOME
|
||||
end
|
||||
|
||||
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
|
||||
set -e _OLD_FISH_PROMPT_OVERRIDE
|
||||
# prevents error when using nested fish instances (Issue #93858)
|
||||
if functions -q _old_fish_prompt
|
||||
functions -e fish_prompt
|
||||
functions -c _old_fish_prompt fish_prompt
|
||||
functions -e _old_fish_prompt
|
||||
end
|
||||
end
|
||||
|
||||
set -e VIRTUAL_ENV
|
||||
set -e VIRTUAL_ENV_PROMPT
|
||||
if test "$argv[1]" != "nondestructive"
|
||||
# Self-destruct!
|
||||
functions -e deactivate
|
||||
end
|
||||
end
|
||||
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
set -gx VIRTUAL_ENV "/home/pac/thrillwiki/thrillwiki_django_no_react/.venv"
|
||||
|
||||
set -gx _OLD_VIRTUAL_PATH $PATH
|
||||
set -gx PATH "$VIRTUAL_ENV/bin" $PATH
|
||||
|
||||
# Unset PYTHONHOME if set.
|
||||
if set -q PYTHONHOME
|
||||
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
|
||||
set -e PYTHONHOME
|
||||
end
|
||||
|
||||
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
|
||||
# fish uses a function instead of an env var to generate the prompt.
|
||||
|
||||
# Save the current fish_prompt function as the function _old_fish_prompt.
|
||||
functions -c fish_prompt _old_fish_prompt
|
||||
|
||||
# With the original prompt function renamed, we can override with our own.
|
||||
function fish_prompt
|
||||
# Save the return status of the last command.
|
||||
set -l old_status $status
|
||||
|
||||
# Output the venv prompt; color taken from the blue of the Python logo.
|
||||
printf "%s%s%s" (set_color 4B8BBE) "(.venv) " (set_color normal)
|
||||
|
||||
# Restore the return status of the previous command.
|
||||
echo "exit $old_status" | .
|
||||
# Output the original/"old" prompt.
|
||||
_old_fish_prompt
|
||||
end
|
||||
|
||||
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
|
||||
set -gx VIRTUAL_ENV_PROMPT "(.venv) "
|
||||
end
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from automat._visualize import tool
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(tool())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from black import patched_main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(patched_main())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from blackd import patched_main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(patched_main())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from twisted.conch.scripts.cftp import run
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(run())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from twisted.conch.scripts.ckeygen import run
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(run())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from twisted.conch.scripts.conch import run
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(run())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from coverage.cmdline import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from coverage.cmdline import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from coverage.cmdline import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from daphne.cli import CommandLineInterface
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(CommandLineInterface.entrypoint())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from django.core.management import execute_from_command_line
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(execute_from_command_line())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from dotenv.__main__ import cli
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(cli())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from flake8.main.cli import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from twisted.mail.scripts.mailmail import run
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(run())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from charset_normalizer.cli import cli_detect
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(cli_detect())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from pytest import console_main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(console_main())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from pycodestyle import _main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(_main())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from pyflakes.api import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from twisted.scripts.htmlizer import run
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(run())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from pytest import console_main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(console_main())
|
||||
@@ -1 +0,0 @@
|
||||
python3
|
||||
@@ -1 +0,0 @@
|
||||
/usr/local/bin/python3
|
||||
@@ -1 +0,0 @@
|
||||
python3
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from sqlparse.__main__ import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from twisted.conch.scripts.tkconch import run
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(run())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from twisted.scripts.trial import run
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(run())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from twisted.application.twist._twist import Twist
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(Twist.main())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from twisted.scripts.twistd import run
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(run())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from autobahn.__main__ import _main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(_main())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from autobahn.xbr._cli import _main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(_main())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/home/pac/thrillwiki/thrillwiki_django_no_react/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from autobahn.xbr._gui import _main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(_main())
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
pip
|
||||
@@ -1,21 +0,0 @@
|
||||
Copyright (c) 2014
|
||||
Rackspace
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,199 +0,0 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: Automat
|
||||
Version: 24.8.1
|
||||
Summary: Self-service finite-state machines for the programmer on the go.
|
||||
Author-email: Glyph <code@glyph.im>
|
||||
License: Copyright (c) 2014
|
||||
Rackspace
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Project-URL: Documentation, https://automat.readthedocs.io/
|
||||
Project-URL: Source, https://github.com/glyph/automat/
|
||||
Keywords: fsm,state machine,automata
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: 3.12
|
||||
Requires-Python: >=3.8
|
||||
Description-Content-Type: text/markdown
|
||||
License-File: LICENSE
|
||||
Requires-Dist: typing-extensions; python_version < "3.10"
|
||||
Provides-Extra: visualize
|
||||
Requires-Dist: graphviz>0.5.1; extra == "visualize"
|
||||
Requires-Dist: Twisted>=16.1.1; extra == "visualize"
|
||||
|
||||
# Automat #
|
||||
|
||||
[](http://automat.readthedocs.io/en/latest/)
|
||||
[](https://github.com/glyph/automat/actions/workflows/ci.yml?query=branch%3Atrunk)
|
||||
[](http://codecov.io/github/glyph/automat?branch=trunk)
|
||||
|
||||
## Self-service finite-state machines for the programmer on the go. ##
|
||||
|
||||
Automat is a library for concise, idiomatic Python expression of finite-state
|
||||
automata (particularly deterministic finite-state transducers).
|
||||
|
||||
Read more here, or on [Read the Docs](https://automat.readthedocs.io/), or watch the following videos for an overview and presentation
|
||||
|
||||
### Why use state machines? ###
|
||||
|
||||
Sometimes you have to create an object whose behavior varies with its state,
|
||||
but still wishes to present a consistent interface to its callers.
|
||||
|
||||
For example, let's say you're writing the software for a coffee machine. It
|
||||
has a lid that can be opened or closed, a chamber for water, a chamber for
|
||||
coffee beans, and a button for "brew".
|
||||
|
||||
There are a number of possible states for the coffee machine. It might or
|
||||
might not have water. It might or might not have beans. The lid might be open
|
||||
or closed. The "brew" button should only actually attempt to brew coffee in
|
||||
one of these configurations, and the "open lid" button should only work if the
|
||||
coffee is not, in fact, brewing.
|
||||
|
||||
With diligence and attention to detail, you can implement this correctly using
|
||||
a collection of attributes on an object; `hasWater`, `hasBeans`, `isLidOpen`
|
||||
and so on. However, you have to keep all these attributes consistent. As the
|
||||
coffee maker becomes more complex - perhaps you add an additional chamber for
|
||||
flavorings so you can make hazelnut coffee, for example - you have to keep
|
||||
adding more and more checks and more and more reasoning about which
|
||||
combinations of states are allowed.
|
||||
|
||||
Rather than adding tedious `if` checks to every single method to make sure that
|
||||
each of these flags are exactly what you expect, you can use a state machine to
|
||||
ensure that if your code runs at all, it will be run with all the required
|
||||
values initialized, because they have to be called in the order you declare
|
||||
them.
|
||||
|
||||
You can read about state machines and their advantages for Python programmers
|
||||
in more detail [in this excellent article by Jean-Paul
|
||||
Calderone](https://web.archive.org/web/20160507053658/https://clusterhq.com/2013/12/05/what-is-a-state-machine/).
|
||||
|
||||
### What makes Automat different? ###
|
||||
|
||||
There are
|
||||
[dozens of libraries on PyPI implementing state machines](https://pypi.org/search/?q=finite+state+machine).
|
||||
So it behooves me to say why yet another one would be a good idea.
|
||||
|
||||
Automat is designed around this principle: while organizing your code around
|
||||
state machines is a good idea, your callers don't, and shouldn't have to, care
|
||||
that you've done so. In Python, the "input" to a stateful system is a method
|
||||
call; the "output" may be a method call, if you need to invoke a side effect,
|
||||
or a return value, if you are just performing a computation in memory. Most
|
||||
other state-machine libraries require you to explicitly create an input object,
|
||||
provide that object to a generic "input" method, and then receive results,
|
||||
sometimes in terms of that library's interfaces and sometimes in terms of
|
||||
classes you define yourself.
|
||||
|
||||
For example, a snippet of the coffee-machine example above might be implemented
|
||||
as follows in naive Python:
|
||||
|
||||
```python
|
||||
class CoffeeMachine(object):
|
||||
def brewButton(self) -> None:
|
||||
if self.hasWater and self.hasBeans and not self.isLidOpen:
|
||||
self.heatTheHeatingElement()
|
||||
# ...
|
||||
```
|
||||
|
||||
With Automat, you'd begin with a `typing.Protocol` that describes all of your
|
||||
inputs:
|
||||
|
||||
```python
|
||||
from typing import Protocol
|
||||
|
||||
class CoffeeBrewer(Protocol):
|
||||
def brewButton(self) -> None:
|
||||
"The user pressed the 'brew' button."
|
||||
def putInBeans(self) -> None:
|
||||
"The user put in some beans."
|
||||
```
|
||||
|
||||
We'll then need a concrete class to contain the shared core of state shared
|
||||
among the different states:
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class BrewerCore:
|
||||
heatingElement: HeatingElement
|
||||
```
|
||||
|
||||
Next, we need to describe our state machine, including all of our states. For
|
||||
simplicity's sake let's say that the only two states are `noBeans` and
|
||||
`haveBeans`:
|
||||
|
||||
```python
|
||||
from automat import TypeMachineBuilder
|
||||
|
||||
builder = TypeMachineBuilder(CoffeeBrewer, BrewerCore)
|
||||
noBeans = builder.state("noBeans")
|
||||
haveBeans = builder.state("haveBeans")
|
||||
```
|
||||
|
||||
Next we can describe a simple transition; when we put in beans, we move to the
|
||||
`haveBeans` state, with no other behavior.
|
||||
|
||||
```python
|
||||
# When we don't have beans, upon putting in beans, we will then have beans
|
||||
noBeans.upon(CoffeeBrewer.putInBeans).to(haveBeans).returns(None)
|
||||
```
|
||||
|
||||
And then another transition that we describe with a decorator, one that *does*
|
||||
have some behavior, that needs to heat up the heating element to brew the
|
||||
coffee:
|
||||
|
||||
```python
|
||||
@haveBeans.upon(CoffeeBrewer.brewButton).to(noBeans)
|
||||
def heatUp(inputs: CoffeeBrewer, core: BrewerCore) -> None:
|
||||
"""
|
||||
When we have beans, upon pressing the brew button, we will then not have
|
||||
beans any more (as they have been entered into the brewing chamber) and
|
||||
our output will be heating the heating element.
|
||||
"""
|
||||
print("Brewing the coffee...")
|
||||
core.heatingElement.turnOn()
|
||||
```
|
||||
|
||||
Then we finalize the state machine by building it, which gives us a callable
|
||||
that takes a `BrewerCore` and returns a synthetic `CoffeeBrewer`
|
||||
|
||||
```python
|
||||
newCoffeeMachine = builder.build()
|
||||
```
|
||||
|
||||
```python
|
||||
>>> coffee = newCoffeeMachine(BrewerCore(HeatingElement()))
|
||||
>>> machine.putInBeans()
|
||||
>>> machine.brewButton()
|
||||
Brewing the coffee...
|
||||
```
|
||||
|
||||
All of the *inputs* are provided by calling them like methods, all of the
|
||||
*output behaviors* are automatically invoked when they are produced according
|
||||
to the outputs specified to `upon` and all of the states are simply opaque
|
||||
tokens.
|
||||
@@ -1,39 +0,0 @@
|
||||
../../../bin/automat-visualize,sha256=IsKluasmdX-znO8xCKiVIZ1zYKnBxs3ykFbJ3SxJ4vs,266
|
||||
Automat-24.8.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
Automat-24.8.1.dist-info/LICENSE,[AWS-SECRET-REMOVED]zv_-9TA43Y,1053
|
||||
Automat-24.8.1.dist-info/METADATA,sha256=XJIxL4Olb15WCoAdVEcORum39__wiCXQR6LNubERZ6M,8396
|
||||
Automat-24.8.1.dist-info/RECORD,,
|
||||
Automat-24.8.1.dist-info/WHEEL,sha256=HiCZjzuy6Dw0hdX5R3LCFPDmFS4BWl8H-8W39XfmgX4,91
|
||||
Automat-24.8.1.dist-info/entry_points.txt,[AWS-SECRET-REMOVED]rPNlGi7NbQ,62
|
||||
Automat-24.8.1.dist-info/top_level.txt,sha256=vg4zAOyhP_3YCmpKZLNgFw1uMF3lC_b6TKsdz7jBSpI,8
|
||||
automat/__init__.py,[AWS-SECRET-REMOVED]WD_RltPB-U,356
|
||||
automat/__pycache__/__init__.cpython-312.pyc,,
|
||||
automat/__pycache__/_core.cpython-312.pyc,,
|
||||
automat/__pycache__/_discover.cpython-312.pyc,,
|
||||
automat/__pycache__/_introspection.cpython-312.pyc,,
|
||||
automat/__pycache__/_methodical.cpython-312.pyc,,
|
||||
automat/__pycache__/_runtimeproto.cpython-312.pyc,,
|
||||
automat/__pycache__/_typed.cpython-312.pyc,,
|
||||
automat/__pycache__/_visualize.cpython-312.pyc,,
|
||||
automat/_core.py,sha256=oe4QNlfvmgsnKe_8fyNiOsHsfz5xPArGuXWle9zePp8,6663
|
||||
automat/_discover.py,sha256=KRbmm7kxpd-WReDQU4qe6hVKGUKmGBHUjYIkRneO4mc,5197
|
||||
automat/_introspection.py,sha256=uF5ymY-GZckyRxvRs7UToPBV_oVV6xHmvlBVey9nv80,1441
|
||||
automat/_methodical.py,sha256=YgZXraDe6dvK6w_Y9xfPa0kXCka4ZeGLfcb4IfK4bnI,17243
|
||||
automat/_runtimeproto.py,sha256=mJ_[AWS-SECRET-REMOVED],1654
|
||||
automat/_test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
automat/_test/__pycache__/__init__.cpython-312.pyc,,
|
||||
automat/_test/__pycache__/test_core.cpython-312.pyc,,
|
||||
automat/_test/__pycache__/test_discover.cpython-312.pyc,,
|
||||
automat/_test/__pycache__/test_methodical.cpython-312.pyc,,
|
||||
automat/_test/__pycache__/test_trace.cpython-312.pyc,,
|
||||
automat/_test/__pycache__/test_type_based.cpython-312.pyc,,
|
||||
automat/_test/__pycache__/test_visualize.cpython-312.pyc,,
|
||||
automat/_test/test_core.py,sha256=PJHNvQ85i8vjH-oF6nPNKB84_noTyl2dQSv_iRl70J8,3481
|
||||
automat/_test/test_discover.py,[AWS-SECRET-REMOVED]Gg7kQnbvtw,22067
|
||||
automat/_test/test_methodical.py,sha256=SKMMbl-6bjH-QZ1hDFRItCOyKF4SstuA4QvExnVhn7w,20267
|
||||
automat/_test/test_trace.py,sha256=tty7P_ctJtk38ZXnpmEy-J9Rn-Hh2AKb_ia6EmtXSQI,3299
|
||||
automat/_test/test_type_based.py,[AWS-SECRET-REMOVED]AegU_OUvko,17872
|
||||
automat/_test/test_visualize.py,sha256=HXBPgMAD0OTz_l1Yq0lI3d1vJ_l3sHTH67Jauaf-2sk,14631
|
||||
automat/_typed.py,sha256=lMzMgUfX713Xw_W4pr8iyPqcdpRSbu4rEpRlrXAOW2k,24204
|
||||
automat/_visualize.py,sha256=DQYig2mBKX-LquPEEy89Y_qyj21tSutAUaFsF1F64ws,6512
|
||||
automat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
@@ -1,5 +0,0 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: setuptools (72.2.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
[console_scripts]
|
||||
automat-visualize = automat._visualize:tool
|
||||
@@ -1 +0,0 @@
|
||||
automat
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
pip
|
||||
@@ -1,27 +0,0 @@
|
||||
Copyright (c) Django Software Foundation and individual contributors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of Django nor the names of its contributors may be used
|
||||
to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -1,288 +0,0 @@
|
||||
Django is licensed under the three-clause BSD license; see the file
|
||||
LICENSE for details.
|
||||
|
||||
Django includes code from the Python standard library, which is licensed under
|
||||
the Python license, a permissive open source license. The copyright and license
|
||||
is included below for compliance with Python's terms.
|
||||
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Copyright (c) 2001-present Python Software Foundation; All Rights Reserved
|
||||
|
||||
A. HISTORY OF THE SOFTWARE
|
||||
==========================
|
||||
|
||||
Python was created in the early 1990s by Guido van Rossum at Stichting
|
||||
Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands
|
||||
as a successor of a language called ABC. Guido remains Python's
|
||||
principal author, although it includes many contributions from others.
|
||||
|
||||
In 1995, Guido continued his work on Python at the Corporation for
|
||||
National Research Initiatives (CNRI, see https://www.cnri.reston.va.us)
|
||||
in Reston, Virginia where he released several versions of the
|
||||
software.
|
||||
|
||||
In May 2000, Guido and the Python core development team moved to
|
||||
BeOpen.com to form the BeOpen PythonLabs team. In October of the same
|
||||
year, the PythonLabs team moved to Digital Creations, which became
|
||||
Zope Corporation. In 2001, the Python Software Foundation (PSF, see
|
||||
https://www.python.org/psf/) was formed, a non-profit organization
|
||||
created specifically to own Python-related Intellectual Property.
|
||||
Zope Corporation was a sponsoring member of the PSF.
|
||||
|
||||
All Python releases are Open Source (see https://opensource.org for
|
||||
the Open Source Definition). Historically, most, but not all, Python
|
||||
releases have also been GPL-compatible; the table below summarizes
|
||||
the various releases.
|
||||
|
||||
Release Derived Year Owner GPL-
|
||||
from compatible? (1)
|
||||
|
||||
0.9.0 thru 1.2 1991-1995 CWI yes
|
||||
1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
|
||||
1.6 1.5.2 2000 CNRI no
|
||||
2.0 1.6 2000 BeOpen.com no
|
||||
1.6.1 1.6 2001 CNRI yes (2)
|
||||
2.1 2.0+1.6.1 2001 PSF no
|
||||
2.0.1 2.0+1.6.1 2001 PSF yes
|
||||
2.1.1 2.1+2.0.1 2001 PSF yes
|
||||
2.1.2 2.1.1 2002 PSF yes
|
||||
2.1.3 2.1.2 2002 PSF yes
|
||||
2.2 and above 2.1.1 2001-now PSF yes
|
||||
|
||||
Footnotes:
|
||||
|
||||
(1) GPL-compatible doesn't mean that we're distributing Python under
|
||||
the GPL. All Python licenses, unlike the GPL, let you distribute
|
||||
a modified version without making your changes open source. The
|
||||
GPL-compatible licenses make it possible to combine Python with
|
||||
other software that is released under the GPL; the others don't.
|
||||
|
||||
(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
|
||||
because its license has a choice of law clause. According to
|
||||
CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
|
||||
is "not incompatible" with the GPL.
|
||||
|
||||
Thanks to the many outside volunteers who have worked under Guido's
|
||||
direction to make these releases possible.
|
||||
|
||||
|
||||
B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
|
||||
[AWS-SECRET-REMOVED]=======================
|
||||
|
||||
Python software and documentation are licensed under the
|
||||
Python Software Foundation License Version 2.
|
||||
|
||||
Starting with Python 3.8.6, examples, recipes, and other code in
|
||||
the documentation are dual licensed under the PSF License Version 2
|
||||
and the Zero-Clause BSD license.
|
||||
|
||||
Some software incorporated into Python is under different licenses.
|
||||
The licenses are listed with code falling under that license.
|
||||
|
||||
|
||||
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
||||
--------------------------------------------
|
||||
|
||||
1. This LICENSE AGREEMENT is between the Python Software Foundation
|
||||
("PSF"), and the Individual or Organization ("Licensee") accessing and
|
||||
otherwise using this software ("Python") in source or binary form and
|
||||
its associated documentation.
|
||||
|
||||
2. Subject to the terms and conditions of this License Agreement, PSF hereby
|
||||
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
|
||||
analyze, test, perform and/or display publicly, prepare derivative works,
|
||||
distribute, and otherwise use Python alone or in any derivative version,
|
||||
provided, however, that PSF's License Agreement and PSF's notice of copyright,
|
||||
i.e., "Copyright (c) 2001-2024 Python Software Foundation; All Rights Reserved"
|
||||
are retained in Python alone or in any derivative version prepared by Licensee.
|
||||
|
||||
3. In the event Licensee prepares a derivative work that is based on
|
||||
or incorporates Python or any part thereof, and wants to make
|
||||
the derivative work available to others as provided herein, then
|
||||
Licensee hereby agrees to include in any such work a brief summary of
|
||||
the changes made to Python.
|
||||
|
||||
4. PSF is making Python available to Licensee on an "AS IS"
|
||||
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
|
||||
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
|
||||
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||
|
||||
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
|
||||
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||
|
||||
6. This License Agreement will automatically terminate upon a material
|
||||
breach of its terms and conditions.
|
||||
|
||||
7. Nothing in this License Agreement shall be deemed to create any
|
||||
relationship of agency, partnership, or joint venture between PSF and
|
||||
Licensee. This License Agreement does not grant permission to use PSF
|
||||
trademarks or trade name in a trademark sense to endorse or promote
|
||||
products or services of Licensee, or any third party.
|
||||
|
||||
8. By copying, installing or otherwise using Python, Licensee
|
||||
agrees to be bound by the terms and conditions of this License
|
||||
Agreement.
|
||||
|
||||
|
||||
BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
|
||||
-------------------------------------------
|
||||
|
||||
BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
|
||||
|
||||
1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
|
||||
office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
|
||||
Individual or Organization ("Licensee") accessing and otherwise using
|
||||
this software in source or binary form and its associated
|
||||
documentation ("the Software").
|
||||
|
||||
2. Subject to the terms and conditions of this BeOpen Python License
|
||||
Agreement, BeOpen hereby grants Licensee a non-exclusive,
|
||||
royalty-free, world-wide license to reproduce, analyze, test, perform
|
||||
and/or display publicly, prepare derivative works, distribute, and
|
||||
otherwise use the Software alone or in any derivative version,
|
||||
provided, however, that the BeOpen Python License is retained in the
|
||||
Software, alone or in any derivative version prepared by Licensee.
|
||||
|
||||
3. BeOpen is making the Software available to Licensee on an "AS IS"
|
||||
basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
|
||||
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
|
||||
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||
|
||||
4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
|
||||
SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
|
||||
AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
|
||||
DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||
|
||||
5. This License Agreement will automatically terminate upon a material
|
||||
breach of its terms and conditions.
|
||||
|
||||
6. This License Agreement shall be governed by and interpreted in all
|
||||
respects by the law of the State of California, excluding conflict of
|
||||
law provisions. Nothing in this License Agreement shall be deemed to
|
||||
create any relationship of agency, partnership, or joint venture
|
||||
between BeOpen and Licensee. This License Agreement does not grant
|
||||
permission to use BeOpen trademarks or trade names in a trademark
|
||||
sense to endorse or promote products or services of Licensee, or any
|
||||
third party. As an exception, the "BeOpen Python" logos available at
|
||||
http://www.pythonlabs.com/logos.html may be used according to the
|
||||
permissions granted on that web page.
|
||||
|
||||
7. By copying, installing or otherwise using the software, Licensee
|
||||
agrees to be bound by the terms and conditions of this License
|
||||
Agreement.
|
||||
|
||||
|
||||
CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
|
||||
---------------------------------------
|
||||
|
||||
1. This LICENSE AGREEMENT is between the Corporation for National
|
||||
Research Initiatives, having an office at 1895 Preston White Drive,
|
||||
Reston, VA 20191 ("CNRI"), and the Individual or Organization
|
||||
("Licensee") accessing and otherwise using Python 1.6.1 software in
|
||||
source or binary form and its associated documentation.
|
||||
|
||||
2. Subject to the terms and conditions of this License Agreement, CNRI
|
||||
hereby grants Licensee a nonexclusive, royalty-free, world-wide
|
||||
license to reproduce, analyze, test, perform and/or display publicly,
|
||||
prepare derivative works, distribute, and otherwise use Python 1.6.1
|
||||
alone or in any derivative version, provided, however, that CNRI's
|
||||
License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
|
||||
1995-2001 Corporation for National Research Initiatives; All Rights
|
||||
Reserved" are retained in Python 1.6.1 alone or in any derivative
|
||||
version prepared by Licensee. Alternately, in lieu of CNRI's License
|
||||
Agreement, Licensee may substitute the following text (omitting the
|
||||
quotes): "Python 1.6.1 is made available subject to the terms and
|
||||
conditions in CNRI's License Agreement. This Agreement together with
|
||||
Python 1.6.1 may be located on the internet using the following
|
||||
unique, persistent identifier (known as a handle): 1895.22/1013. This
|
||||
Agreement may also be obtained from a proxy server on the internet
|
||||
using the following URL: http://hdl.handle.net/1895.22/1013".
|
||||
|
||||
3. In the event Licensee prepares a derivative work that is based on
|
||||
or incorporates Python 1.6.1 or any part thereof, and wants to make
|
||||
the derivative work available to others as provided herein, then
|
||||
Licensee hereby agrees to include in any such work a brief summary of
|
||||
the changes made to Python 1.6.1.
|
||||
|
||||
4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
|
||||
basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
|
||||
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
|
||||
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||
|
||||
5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||
1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
|
||||
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||
|
||||
6. This License Agreement will automatically terminate upon a material
|
||||
breach of its terms and conditions.
|
||||
|
||||
7. This License Agreement shall be governed by the federal
|
||||
intellectual property law of the United States, including without
|
||||
limitation the federal copyright law, and, to the extent such
|
||||
U.S. federal law does not apply, by the law of the Commonwealth of
|
||||
Virginia, excluding Virginia's conflict of law provisions.
|
||||
Notwithstanding the foregoing, with regard to derivative works based
|
||||
on Python 1.6.1 that incorporate non-separable material that was
|
||||
previously distributed under the GNU General Public License (GPL), the
|
||||
law of the Commonwealth of Virginia shall govern this License
|
||||
Agreement only as to issues arising under or with respect to
|
||||
Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
|
||||
License Agreement shall be deemed to create any relationship of
|
||||
agency, partnership, or joint venture between CNRI and Licensee. This
|
||||
License Agreement does not grant permission to use CNRI trademarks or
|
||||
trade name in a trademark sense to endorse or promote products or
|
||||
services of Licensee, or any third party.
|
||||
|
||||
8. By clicking on the "ACCEPT" button where indicated, or by copying,
|
||||
installing or otherwise using Python 1.6.1, Licensee agrees to be
|
||||
bound by the terms and conditions of this License Agreement.
|
||||
|
||||
ACCEPT
|
||||
|
||||
|
||||
CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
|
||||
--------------------------------------------------
|
||||
|
||||
Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
|
||||
The Netherlands. All rights reserved.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software and its
|
||||
documentation for any purpose and without fee is hereby granted,
|
||||
provided that the above copyright notice appear in all copies and that
|
||||
both that copyright notice and this permission notice appear in
|
||||
supporting documentation, and that the name of Stichting Mathematisch
|
||||
Centrum or CWI not be used in advertising or publicity pertaining to
|
||||
distribution of the software without specific, written prior
|
||||
permission.
|
||||
|
||||
STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||
THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
|
||||
FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
||||
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
@@ -1,100 +0,0 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: Django
|
||||
Version: 5.1.2
|
||||
Summary: A high-level Python web framework that encourages rapid development and clean, pragmatic design.
|
||||
Author-email: Django Software Foundation <foundation@djangoproject.com>
|
||||
License: BSD-3-Clause
|
||||
Project-URL: Homepage, https://www.djangoproject.com/
|
||||
Project-URL: Documentation, https://docs.djangoproject.com/
|
||||
Project-URL: Release notes, https://docs.djangoproject.com/en/stable/releases/
|
||||
Project-URL: Funding, https://www.djangoproject.com/fundraising/
|
||||
Project-URL: Source, https://github.com/django/django
|
||||
Project-URL: Tracker, https://code.djangoproject.com/
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Environment :: Web Environment
|
||||
Classifier: Framework :: Django
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3 :: Only
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: 3.12
|
||||
Classifier: Topic :: Internet :: WWW/HTTP
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
|
||||
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||
Requires-Python: >=3.10
|
||||
Description-Content-Type: text/x-rst
|
||||
License-File: LICENSE
|
||||
License-File: LICENSE.python
|
||||
License-File: AUTHORS
|
||||
Requires-Dist: asgiref<4,>=3.8.1
|
||||
Requires-Dist: sqlparse>=0.3.1
|
||||
Requires-Dist: tzdata; sys_platform == "win32"
|
||||
Provides-Extra: argon2
|
||||
Requires-Dist: argon2-cffi>=19.1.0; extra == "argon2"
|
||||
Provides-Extra: bcrypt
|
||||
Requires-Dist: bcrypt; extra == "bcrypt"
|
||||
|
||||
======
|
||||
Django
|
||||
======
|
||||
|
||||
Django is a high-level Python web framework that encourages rapid development
|
||||
and clean, pragmatic design. Thanks for checking it out.
|
||||
|
||||
All documentation is in the "``docs``" directory and online at
|
||||
https://docs.djangoproject.com/en/stable/. If you're just getting started,
|
||||
here's how we recommend you read the docs:
|
||||
|
||||
* First, read ``docs/intro/install.txt`` for instructions on installing Django.
|
||||
|
||||
* Next, work through the tutorials in order (``docs/intro/tutorial01.txt``,
|
||||
``docs/intro/tutorial02.txt``, etc.).
|
||||
|
||||
* If you want to set up an actual deployment server, read
|
||||
``docs/howto/deployment/index.txt`` for instructions.
|
||||
|
||||
* You'll probably want to read through the topical guides (in ``docs/topics``)
|
||||
next; from there you can jump to the HOWTOs (in ``docs/howto``) for specific
|
||||
problems, and check out the reference (``docs/ref``) for gory details.
|
||||
|
||||
* See ``docs/README`` for instructions on building an HTML version of the docs.
|
||||
|
||||
Docs are updated rigorously. If you find any problems in the docs, or think
|
||||
they should be clarified in any way, please take 30 seconds to fill out a
|
||||
ticket here: https://code.djangoproject.com/newticket
|
||||
|
||||
To get more help:
|
||||
|
||||
* Join the ``#django`` channel on ``irc.libera.chat``. Lots of helpful people
|
||||
hang out there. `Webchat is available <https://web.libera.chat/#django>`_.
|
||||
|
||||
* Join the django-users mailing list, or read the archives, at
|
||||
https://groups.google.com/group/django-users.
|
||||
|
||||
* Join the `Django Discord community <https://discord.gg/xcRH6mN4fa>`_.
|
||||
|
||||
* Join the community on the `Django Forum <https://forum.djangoproject.com/>`_.
|
||||
|
||||
To contribute to Django:
|
||||
|
||||
* Check out https://docs.djangoproject.com/en/dev/internals/contributing/ for
|
||||
information about getting involved.
|
||||
|
||||
To run Django's test suite:
|
||||
|
||||
* Follow the instructions in the "Unit tests" section of
|
||||
``docs/internals/contributing/writing-code/unit-tests.txt``, published online at
|
||||
https://docs.djangoproject.[AWS-SECRET-REMOVED]g-code/unit-tests/#running-the-unit-tests
|
||||
|
||||
Supporting the Development of Django
|
||||
====================================
|
||||
|
||||
Django's development depends on your contributions.
|
||||
|
||||
If you depend on Django, remember to support the Django Software Foundation: https://www.djangoproject.com/fundraising/
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +0,0 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.44.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
[console_scripts]
|
||||
django-admin = django.core.management:execute_from_command_line
|
||||
@@ -1 +0,0 @@
|
||||
django
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,31 +0,0 @@
|
||||
# Copyright (C) AB Strakt
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
pyOpenSSL - A simple wrapper around the OpenSSL library
|
||||
"""
|
||||
|
||||
from OpenSSL import SSL, crypto
|
||||
from OpenSSL.version import (
|
||||
__author__,
|
||||
__copyright__,
|
||||
__email__,
|
||||
__license__,
|
||||
__summary__,
|
||||
__title__,
|
||||
__uri__,
|
||||
__version__,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"SSL",
|
||||
"crypto",
|
||||
"__author__",
|
||||
"__copyright__",
|
||||
"__email__",
|
||||
"__license__",
|
||||
"__summary__",
|
||||
"__title__",
|
||||
"__uri__",
|
||||
"__version__",
|
||||
]
|
||||
@@ -1,124 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
from typing import Any, Callable, NoReturn, Type, Union
|
||||
|
||||
from cryptography.hazmat.bindings.openssl.binding import Binding
|
||||
|
||||
StrOrBytesPath = Union[str, bytes, os.PathLike]
|
||||
|
||||
binding = Binding()
|
||||
ffi = binding.ffi
|
||||
lib = binding.lib
|
||||
|
||||
|
||||
# This is a special CFFI allocator that does not bother to zero its memory
|
||||
# after allocation. This has vastly better performance on large allocations and
|
||||
# so should be used whenever we don't need the memory zeroed out.
|
||||
no_zero_allocator = ffi.new_allocator(should_clear_after_alloc=False)
|
||||
|
||||
|
||||
def text(charp: Any) -> str:
|
||||
"""
|
||||
Get a native string type representing of the given CFFI ``char*`` object.
|
||||
|
||||
:param charp: A C-style string represented using CFFI.
|
||||
|
||||
:return: :class:`str`
|
||||
"""
|
||||
if not charp:
|
||||
return ""
|
||||
return ffi.string(charp).decode("utf-8")
|
||||
|
||||
|
||||
def exception_from_error_queue(exception_type: Type[Exception]) -> NoReturn:
|
||||
"""
|
||||
Convert an OpenSSL library failure into a Python exception.
|
||||
|
||||
When a call to the native OpenSSL library fails, this is usually signalled
|
||||
by the return value, and an error code is stored in an error queue
|
||||
associated with the current thread. The err library provides functions to
|
||||
obtain these error codes and textual error messages.
|
||||
"""
|
||||
errors = []
|
||||
|
||||
while True:
|
||||
error = lib.ERR_get_error()
|
||||
if error == 0:
|
||||
break
|
||||
errors.append(
|
||||
(
|
||||
text(lib.ERR_lib_error_string(error)),
|
||||
text(lib.ERR_func_error_string(error)),
|
||||
text(lib.ERR_reason_error_string(error)),
|
||||
)
|
||||
)
|
||||
|
||||
raise exception_type(errors)
|
||||
|
||||
|
||||
def make_assert(error: Type[Exception]) -> Callable[[bool], Any]:
|
||||
"""
|
||||
Create an assert function that uses :func:`exception_from_error_queue` to
|
||||
raise an exception wrapped by *error*.
|
||||
"""
|
||||
|
||||
def openssl_assert(ok: bool) -> None:
|
||||
"""
|
||||
If *ok* is not True, retrieve the error from OpenSSL and raise it.
|
||||
"""
|
||||
if ok is not True:
|
||||
exception_from_error_queue(error)
|
||||
|
||||
return openssl_assert
|
||||
|
||||
|
||||
def path_bytes(s: StrOrBytesPath) -> bytes:
|
||||
"""
|
||||
Convert a Python path to a :py:class:`bytes` for the path which can be
|
||||
passed into an OpenSSL API accepting a filename.
|
||||
|
||||
:param s: A path (valid for os.fspath).
|
||||
|
||||
:return: An instance of :py:class:`bytes`.
|
||||
"""
|
||||
b = os.fspath(s)
|
||||
|
||||
if isinstance(b, str):
|
||||
return b.encode(sys.getfilesystemencoding())
|
||||
else:
|
||||
return b
|
||||
|
||||
|
||||
def byte_string(s: str) -> bytes:
|
||||
return s.encode("charmap")
|
||||
|
||||
|
||||
# A marker object to observe whether some optional arguments are passed any
|
||||
# value or not.
|
||||
UNSPECIFIED = object()
|
||||
|
||||
_TEXT_WARNING = "str for {0} is no longer accepted, use bytes"
|
||||
|
||||
|
||||
def text_to_bytes_and_warn(label: str, obj: Any) -> Any:
|
||||
"""
|
||||
If ``obj`` is text, emit a warning that it should be bytes instead and try
|
||||
to convert it to bytes automatically.
|
||||
|
||||
:param str label: The name of the parameter from which ``obj`` was taken
|
||||
(so a developer can easily find the source of the problem and correct
|
||||
it).
|
||||
|
||||
:return: If ``obj`` is the text string type, a ``bytes`` object giving the
|
||||
UTF-8 encoding of that text is returned. Otherwise, ``obj`` itself is
|
||||
returned.
|
||||
"""
|
||||
if isinstance(obj, str):
|
||||
warnings.warn(
|
||||
_TEXT_WARNING.format(label),
|
||||
category=DeprecationWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
return obj.encode("utf-8")
|
||||
return obj
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,40 +0,0 @@
|
||||
import ssl
|
||||
import sys
|
||||
|
||||
import cffi
|
||||
import cryptography
|
||||
|
||||
import OpenSSL.SSL
|
||||
|
||||
from . import version
|
||||
|
||||
_env_info = """\
|
||||
pyOpenSSL: {pyopenssl}
|
||||
cryptography: {cryptography}
|
||||
cffi: {cffi}
|
||||
cryptography's compiled against OpenSSL: {crypto_openssl_compile}
|
||||
cryptography's linked OpenSSL: {crypto_openssl_link}
|
||||
Python's OpenSSL: {python_openssl}
|
||||
Python executable: {python}
|
||||
Python version: {python_version}
|
||||
Platform: {platform}
|
||||
sys.path: {sys_path}""".format(
|
||||
pyopenssl=version.__version__,
|
||||
crypto_openssl_compile=OpenSSL._util.ffi.string(
|
||||
OpenSSL._util.lib.OPENSSL_VERSION_TEXT,
|
||||
).decode("ascii"),
|
||||
crypto_openssl_link=OpenSSL.SSL.SSLeay_version(
|
||||
OpenSSL.SSL.SSLEAY_VERSION
|
||||
).decode("ascii"),
|
||||
python_openssl=getattr(ssl, "OPENSSL_VERSION", "n/a"),
|
||||
cryptography=cryptography.__version__,
|
||||
cffi=cffi.__version__,
|
||||
python=sys.executable,
|
||||
python_version=sys.version,
|
||||
platform=sys.platform,
|
||||
sys_path=sys.path,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(_env_info)
|
||||
@@ -1,40 +0,0 @@
|
||||
"""
|
||||
PRNG management routines, thin wrappers.
|
||||
"""
|
||||
|
||||
from OpenSSL._util import lib as _lib
|
||||
|
||||
|
||||
def add(buffer: bytes, entropy: int) -> None:
|
||||
"""
|
||||
Mix bytes from *string* into the PRNG state.
|
||||
|
||||
The *entropy* argument is (the lower bound of) an estimate of how much
|
||||
randomness is contained in *string*, measured in bytes.
|
||||
|
||||
For more information, see e.g. :rfc:`1750`.
|
||||
|
||||
This function is only relevant if you are forking Python processes and
|
||||
need to reseed the CSPRNG after fork.
|
||||
|
||||
:param buffer: Buffer with random data.
|
||||
:param entropy: The entropy (in bytes) measurement of the buffer.
|
||||
|
||||
:return: :obj:`None`
|
||||
"""
|
||||
if not isinstance(buffer, bytes):
|
||||
raise TypeError("buffer must be a byte string")
|
||||
|
||||
if not isinstance(entropy, int):
|
||||
raise TypeError("entropy must be an integer")
|
||||
|
||||
_lib.RAND_add(buffer, len(buffer), entropy)
|
||||
|
||||
|
||||
def status() -> int:
|
||||
"""
|
||||
Check whether the PRNG has been seeded with enough data.
|
||||
|
||||
:return: 1 if the PRNG is seeded enough, 0 otherwise.
|
||||
"""
|
||||
return _lib.RAND_status()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user