1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-23 06:49:37 +02:00

feat: Add Hotel model and integrate into admin panel; update related components

This commit is contained in:
Sean Morley 2025-02-02 10:36:47 -05:00
parent bdb17a3177
commit 659c56f02d
6 changed files with 95 additions and 17 deletions

View file

@ -1,8 +1,8 @@
import os import os
from django.contrib import admin from django.contrib import admin
from django.utils.html import mark_safe from django.utils.html import mark_safe
from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage, Visit, Category, Attachment from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage, Visit, Category, Attachment, Hotel
from worldtravel.models import Country, Region, VisitedRegion, City, VisitedCity from worldtravel.models import Country, Region, VisitedRegion, City, VisitedCity
from allauth.account.decorators import secure_admin_login from allauth.account.decorators import secure_admin_login
admin.autodiscover() admin.autodiscover()
@ -140,6 +140,7 @@ admin.site.register(Category, CategoryAdmin)
admin.site.register(City, CityAdmin) admin.site.register(City, CityAdmin)
admin.site.register(VisitedCity) admin.site.register(VisitedCity)
admin.site.register(Attachment) admin.site.register(Attachment)
admin.site.register(Hotel)
admin.site.site_header = 'AdventureLog Admin' admin.site.site_header = 'AdventureLog Admin'
admin.site.site_title = 'AdventureLog Admin Site' admin.site.site_title = 'AdventureLog Admin Site'

View file

@ -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)),
],
),
]

View file

@ -318,4 +318,37 @@ class Category(models.Model):
def __str__(self): def __str__(self):
return self.name + ' - ' + self.display_name + ' - ' + self.icon 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

View file

@ -13,18 +13,22 @@
let loading = false; let loading = false;
export let adventure: Adventure | null = null; export let adventure: Adventure | null = null;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let albums: ImmichAlbum[] = []; let albums: ImmichAlbum[] = [];
let currentAlbum: string = ''; 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) { if (!selectedDate) {
selectedDate = new Date().toISOString().split('T')[0]; selectedDate = new Date().toISOString().split('T')[0];
} }
$: { $: {
if (currentAlbum) { if (currentAlbum) {
immichImages = []; immichImages = [];
@ -33,7 +37,7 @@
searchImmich(); searchImmich();
} }
} }
async function loadMoreImmich() { 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. // 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); 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}`); return fetchAssets(`/api/integrations/immich/albums/${album_id}`);
} }
@ -82,14 +86,13 @@
} }
}); });
function buildQueryParams() { function buildQueryParams() {
let params = new URLSearchParams(); let params = new URLSearchParams();
if (immichSearchValue && searchCategory === 'search') { if (immichSearchValue && searchCategory === 'search') {
params.append('query', immichSearchValue); params.append('query', immichSearchValue);
} else if (selectedDate && searchCategory === 'date') { } else if (selectedDate && searchCategory === 'date') {
params.append('date', selectedDate); params.append('date', selectedDate);
} }
return params.toString(); return params.toString();
} }
@ -98,9 +101,9 @@
}, 500); // Debounce the search function to avoid multiple requests on every key press }, 500); // Debounce the search function to avoid multiple requests on every key press
async function _searchImmich() { async function _searchImmich() {
immichImages = [];
return fetchAssets(`/api/integrations/immich/search/?${buildQueryParams()}`); return fetchAssets(`/api/integrations/immich/search/?${buildQueryParams()}`);
} }
</script> </script>
<div class="mb-4"> <div class="mb-4">
@ -164,9 +167,11 @@
<p class="text-red-500">{immichError}</p> <p class="text-red-500">{immichError}</p>
<div class="flex flex-wrap gap-4 mr-4 mt-2"> <div class="flex flex-wrap gap-4 mr-4 mt-2">
{#if loading} {#if loading}
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-[100] w-24 h-24"> <div
<span class="loading loading-spinner w-24 h-24"></span> class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-[100] w-24 h-24"
</div> >
<span class="loading loading-spinner w-24 h-24"></span>
</div>
{/if} {/if}
{#each immichImages as image} {#each immichImages as image}
@ -178,7 +183,7 @@
class="h-24 w-24 object-cover rounded-md" class="h-24 w-24 object-cover rounded-md"
/> />
<h4> <h4>
{image.fileCreatedAt?.split('T')[0] || "Unknown"} {image.fileCreatedAt?.split('T')[0] || 'Unknown'}
</h4> </h4>
<button <button
type="button" type="button"

View file

@ -50,7 +50,7 @@
<!-- Card Actions --> <!-- Card Actions -->
<div class="card-actions justify-center mt-6"> <div class="card-actions justify-center mt-6">
{#if !sharing} {#if !sharing}
<button class="btn btn-primary" on:click={() => goto(`/user/${user.uuid}`)}> <button class="btn btn-primary" on:click={() => goto(`/profile/${user.username}`)}>
View Profile View Profile
</button> </button>
{:else if shared_with && !shared_with.includes(user.uuid)} {:else if shared_with && !shared_with.includes(user.uuid)}

View file

@ -40,7 +40,7 @@ export type Adventure = {
is_visited?: boolean; is_visited?: boolean;
category: Category | null; category: Category | null;
attachments: Attachment[]; attachments: Attachment[];
user: User user?: User | null;
}; };
export type Country = { export type Country = {