# Collections & Sharing Domain ## Collection Sharing Architecture ### Data Model - `Collection.shared_with` - `ManyToManyField(User, related_name='shared_with', blank=True)` - the primary access grant - `CollectionInvite` - staging table for pending invites: `(collection FK, invited_user FK, unique_together)`; prevents self-invite and double-invite - Both models in `backend/server/adventures/models.py` ### Access Flow (Invite -> Accept -> Membership) 1. Owner calls `POST /api/collections/{id}/share/{uuid}/` -> creates `CollectionInvite` 2. Invited user sees pending invites via `GET /api/collections/invites/` 3. Invited user calls `POST /api/collections/{id}/accept-invite/` -> adds to `shared_with`, deletes invite 4. Owner revokes: `POST /api/collections/{id}/revoke-invite/{uuid}/` 5. User declines: `POST /api/collections/{id}/decline-invite/` 6. Owner removes: `POST /api/collections/{id}/unshare/{uuid}/` - removes user's locations from collection 7. User self-removes: `POST /api/collections/{id}/leave/` - removes their locations ### Permission Classes - `CollectionShared` - full access for owner and `shared_with` members; invite actions for invitees; anonymous read for `is_public` - `IsOwnerOrSharedWithFullAccess` - child-object viewsets; traverses `obj.collections`/`obj.collection` to check `shared_with` - `ContentImagePermission` - delegates to `IsOwnerOrSharedWithFullAccess` on `content_object` ### Key Design Constraints - No role differentiation - all shared users have same write access - On unshare/leave, departing user's locations are removed from collection (not deleted) - `duplicate` action creates a private copy with no `shared_with` transfer ### Chat Agent Tool Access - `get_trip_details` and `add_to_itinerary` tools authorize using `Q(user=user) | Q(shared_with=user)` — shared members can use AI chat tools on shared collections. - `list_trips` remains owner-only (shared collections not listed). - `add_to_itinerary` assigns `Location.user = shared_user` (shared users own their contributed locations), consistent with REST API behavior. - See [patterns/chat-and-llm.md](../patterns/chat-and-llm.md#shared-trip-tool-access). ## Itinerary Architecture ### Primary Component `CollectionItineraryPlanner.svelte` (~3818 lines) - monolith rendering the entire itinerary view and all modals. ### The "Add" Button DaisyUI dropdown at bottom of each day card with "Link existing item" and "Create new" submenu (Location, Lodging, Transportation, Note, Checklist). ### Day Suggestions Modal (WS3) - Component: `ItinerarySuggestionModal.svelte` - Props: `collection`, `user`, `targetDate`, `displayDate` - Events: `close`, `addItem { type: 'location', itemId, updateDate }` - 3-step UX: category selection -> filters -> results from `POST /api/chat/suggestions/day/` ## User Recommendation Preference Profile Backend-only feature: model, API, and system-prompt integration exist, but **no frontend UI** built yet. ### Auto-learned preference updates - `backend/server/integrations/utils/auto_profile.py` derives profile from user history - Fields: `interests` (from activities + locations), `trip_style` (from lodging), `notes` (frequently visited regions) - `ChatViewSet.send_message()` invokes `update_auto_preference_profile(request.user)` at method start ### Model `UserRecommendationPreferenceProfile` (OneToOne -> `CustomUser`): `cuisines`, `interests` (JSONField), `trip_style`, `notes`, timestamps. ### System Prompt Integration - Single-user: labeled as auto-inferred with structured markdown section - Shared collections: `get_aggregated_preferences(collection)` appends per-participant preferences - Missing profiles skipped gracefully ### Frontend Gap - No settings tab for manual preference editing - TypeScript type available as `UserRecommendationPreferenceProfile` in `src/lib/types.ts` - See [Plan: AI travel agent redesign](../../plans/ai-travel-agent-redesign.md#ws2-user-preference-learning)