From 659c56f02d8d3e1013480dd12e18ee850ef5c195 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Sun, 2 Feb 2025 10:36:47 -0500 Subject: [PATCH] feat: Add Hotel model and integrate into admin panel; update related components --- backend/server/adventures/admin.py | 5 ++- .../adventures/migrations/0022_hotel.py | 39 +++++++++++++++++++ backend/server/adventures/models.py | 35 ++++++++++++++++- .../src/lib/components/ImmichSelect.svelte | 29 ++++++++------ frontend/src/lib/components/UserCard.svelte | 2 +- frontend/src/lib/types.ts | 2 +- 6 files changed, 95 insertions(+), 17 deletions(-) create mode 100644 backend/server/adventures/migrations/0022_hotel.py diff --git a/backend/server/adventures/admin.py b/backend/server/adventures/admin.py index a1a1101f..51c9bac8 100644 --- a/backend/server/adventures/admin.py +++ b/backend/server/adventures/admin.py @@ -1,8 +1,8 @@ import os from django.contrib import admin from django.utils.html import mark_safe -from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage, Visit, Category, Attachment -from worldtravel.models import Country, Region, VisitedRegion, City, VisitedCity +from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage, Visit, Category, Attachment, Hotel +from worldtravel.models import Country, Region, VisitedRegion, City, VisitedCity from allauth.account.decorators import secure_admin_login admin.autodiscover() @@ -140,6 +140,7 @@ def adventure_count(self, obj): admin.site.register(City, CityAdmin) admin.site.register(VisitedCity) admin.site.register(Attachment) +admin.site.register(Hotel) admin.site.site_header = 'AdventureLog Admin' admin.site.site_title = 'AdventureLog Admin Site' diff --git a/backend/server/adventures/migrations/0022_hotel.py b/backend/server/adventures/migrations/0022_hotel.py new file mode 100644 index 00000000..56a60979 --- /dev/null +++ b/backend/server/adventures/migrations/0022_hotel.py @@ -0,0 +1,39 @@ +# Generated by Django 5.0.8 on 2025-02-02 15:36 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('adventures', '0021_alter_attachment_name'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Hotel', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('name', models.CharField(max_length=200)), + ('description', models.TextField(blank=True, null=True)), + ('rating', models.FloatField(blank=True, null=True)), + ('link', models.URLField(blank=True, max_length=2083, null=True)), + ('check_in', models.DateTimeField(blank=True, null=True)), + ('check_out', models.DateTimeField(blank=True, null=True)), + ('reservation_number', models.CharField(blank=True, max_length=100, null=True)), + ('price', models.DecimalField(blank=True, decimal_places=2, max_digits=9, null=True)), + ('latitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)), + ('longitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)), + ('location', models.CharField(blank=True, max_length=200, null=True)), + ('is_public', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('collection', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='adventures.collection')), + ('user_id', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/backend/server/adventures/models.py b/backend/server/adventures/models.py index af0d7b97..96d439b9 100644 --- a/backend/server/adventures/models.py +++ b/backend/server/adventures/models.py @@ -318,4 +318,37 @@ def clean(self) -> None: def __str__(self): - return self.name + ' - ' + self.display_name + ' - ' + self.icon \ No newline at end of file + return self.name + ' - ' + self.display_name + ' - ' + self.icon + +class Hotel(models.Model): + id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True) + user_id = models.ForeignKey( + User, on_delete=models.CASCADE, default=default_user_id) + name = models.CharField(max_length=200) + description = models.TextField(blank=True, null=True) + rating = models.FloatField(blank=True, null=True) + link = models.URLField(blank=True, null=True, max_length=2083) + check_in = models.DateTimeField(blank=True, null=True) + check_out = models.DateTimeField(blank=True, null=True) + reservation_number = models.CharField(max_length=100, blank=True, null=True) + price = models.DecimalField(max_digits=9, decimal_places=2, blank=True, null=True) + latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) + longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) + location = models.CharField(max_length=200, blank=True, null=True) + is_public = models.BooleanField(default=False) + collection = models.ForeignKey('Collection', on_delete=models.CASCADE, blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def clean(self): + if self.date and self.end_date and self.date > self.end_date: + raise ValidationError('The start date must be before the end date. Start date: ' + str(self.date) + ' End date: ' + str(self.end_date)) + + if self.collection: + if self.collection.is_public and not self.is_public: + raise ValidationError('Hotels associated with a public collection must be public. Collection: ' + self.collection.name + ' Hotel: ' + self.name) + if self.user_id != self.collection.user_id: + raise ValidationError('Hotels must be associated with collections owned by the same user. Collection owner: ' + self.collection.user_id.username + ' Hotel owner: ' + self.user_id.username) + + def __str__(self): + return self.name \ No newline at end of file diff --git a/frontend/src/lib/components/ImmichSelect.svelte b/frontend/src/lib/components/ImmichSelect.svelte index d6b6221a..b1d137da 100644 --- a/frontend/src/lib/components/ImmichSelect.svelte +++ b/frontend/src/lib/components/ImmichSelect.svelte @@ -13,18 +13,22 @@ let loading = false; export let adventure: Adventure | null = null; - + const dispatch = createEventDispatcher(); let albums: ImmichAlbum[] = []; let currentAlbum: string = ''; - let selectedDate: string = (adventure as Adventure | null)?.visits.map(v => new Date(v.end_date || v.start_date)).sort((a,b) => +b - +a)[0]?.toISOString()?.split('T')[0] || ''; + let selectedDate: string = + (adventure as Adventure | null)?.visits + .map((v) => new Date(v.end_date || v.start_date)) + .sort((a, b) => +b - +a)[0] + ?.toISOString() + ?.split('T')[0] || ''; if (!selectedDate) { selectedDate = new Date().toISOString().split('T')[0]; } - $: { if (currentAlbum) { immichImages = []; @@ -33,7 +37,7 @@ searchImmich(); } } - + async function loadMoreImmich() { // The next URL returned by our API is a absolute url to API, but we need to use the relative path, to use the frontend api proxy. const url = new URL(immichNextURL); @@ -70,7 +74,7 @@ } } - async function fetchAlbumAssets(album_id: string,) { + async function fetchAlbumAssets(album_id: string) { return fetchAssets(`/api/integrations/immich/albums/${album_id}`); } @@ -82,14 +86,13 @@ } }); - function buildQueryParams() { let params = new URLSearchParams(); if (immichSearchValue && searchCategory === 'search') { params.append('query', immichSearchValue); } else if (selectedDate && searchCategory === 'date') { params.append('date', selectedDate); - } + } return params.toString(); } @@ -98,9 +101,9 @@ }, 500); // Debounce the search function to avoid multiple requests on every key press async function _searchImmich() { + immichImages = []; return fetchAssets(`/api/integrations/immich/search/?${buildQueryParams()}`); } -
@@ -164,9 +167,11 @@

{immichError}

{#if loading} -
- -
+
+ +
{/if} {#each immichImages as image} @@ -178,7 +183,7 @@ class="h-24 w-24 object-cover rounded-md" />

- {image.fileCreatedAt?.split('T')[0] || "Unknown"} + {image.fileCreatedAt?.split('T')[0] || 'Unknown'}

{:else if shared_with && !shared_with.includes(user.uuid)} diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index f56125a7..ab512a4f 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -40,7 +40,7 @@ export type Adventure = { is_visited?: boolean; category: Category | null; attachments: Attachment[]; - user: User + user?: User | null; }; export type Country = {