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:
parent
bdb17a3177
commit
659c56f02d
6 changed files with 95 additions and 17 deletions
|
@ -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'
|
||||||
|
|
39
backend/server/adventures/migrations/0022_hotel.py
Normal file
39
backend/server/adventures/migrations/0022_hotel.py
Normal 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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -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
|
|
@ -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"
|
||||||
|
|
|
@ -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)}
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue