1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-19 04: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
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 @@ admin.site.register(Category, CategoryAdmin)
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'

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):
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;
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()}`);
}
</script>
<div class="mb-4">
@ -164,9 +167,11 @@
<p class="text-red-500">{immichError}</p>
<div class="flex flex-wrap gap-4 mr-4 mt-2">
{#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">
<span class="loading loading-spinner w-24 h-24"></span>
</div>
<div
class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-[100] w-24 h-24"
>
<span class="loading loading-spinner w-24 h-24"></span>
</div>
{/if}
{#each immichImages as image}
@ -178,7 +183,7 @@
class="h-24 w-24 object-cover rounded-md"
/>
<h4>
{image.fileCreatedAt?.split('T')[0] || "Unknown"}
{image.fileCreatedAt?.split('T')[0] || 'Unknown'}
</h4>
<button
type="button"

View file

@ -50,7 +50,7 @@
<!-- Card Actions -->
<div class="card-actions justify-center mt-6">
{#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
</button>
{:else if shared_with && !shared_with.includes(user.uuid)}

View file

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