1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-30 18:29:37 +02:00

feat: Add additional adventure type and endpoint for sunrise/sunset information

This commit is contained in:
Sean Morley 2025-03-22 12:25:53 -04:00
parent 13d3b24ec2
commit 16a7772003
5 changed files with 146 additions and 95 deletions

View file

@ -10,6 +10,7 @@ from adventures.models import Adventure, Category, Transportation, Lodging
from adventures.permissions import IsOwnerOrSharedWithFullAccess from adventures.permissions import IsOwnerOrSharedWithFullAccess
from adventures.serializers import AdventureSerializer, TransportationSerializer, LodgingSerializer from adventures.serializers import AdventureSerializer, TransportationSerializer, LodgingSerializer
from adventures.utils import pagination from adventures.utils import pagination
import requests
class AdventureViewSet(viewsets.ModelViewSet): class AdventureViewSet(viewsets.ModelViewSet):
serializer_class = AdventureSerializer serializer_class = AdventureSerializer
@ -170,48 +171,38 @@ class AdventureViewSet(viewsets.ModelViewSet):
serializer = self.get_serializer(queryset, many=True) serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data) return Response(serializer.data)
# @action(detail=True, methods=['post']) @action(detail=True, methods=['get'], url_path='additional-info')
# def convert(self, request, pk=None): def additional_info(self, request, pk=None):
# """ adventure = self.get_object()
# Convert an Adventure instance into a Transportation or Lodging instance.
# Expects a JSON body with "target_type": "transportation" or "lodging".
# """
# adventure = self.get_object()
# target_type = request.data.get("target_type", "").lower()
# if target_type not in ["transportation", "lodging"]: # Permission check: owner or shared collection member
# return Response( if adventure.user_id != request.user:
# {"error": "Invalid target type. Must be 'transportation' or 'lodging'."}, if not (adventure.collection and adventure.collection.shared_with.filter(id=request.user.id).exists()):
# status=400 return Response({"error": "User does not have permission to access this adventure"},
# ) status=status.HTTP_403_FORBIDDEN)
# if not adventure.collection:
# return Response(
# {"error": "Adventure must be part of a collection to be converted."},
# status=400
# )
# # Define the overlapping fields that both the Adventure and target models share. serializer = self.get_serializer(adventure)
# overlapping_fields = ["name", "description", "is_public", 'collection'] response_data = serializer.data
# # Gather the overlapping data from the adventure instance. visits = response_data.get('visits', [])
# conversion_data = {} sun_times = []
# for field in overlapping_fields:
# if hasattr(adventure, field):
# conversion_data[field] = getattr(adventure, field)
# # Make sure to include the user reference for visit in visits:
# conversion_data["user_id"] = adventure.user_id date = visit.get('start_date')
if date and adventure.longitude and adventure.latitude:
api_url = f'https://api.sunrisesunset.io/json?lat={adventure.latitude}&lng={adventure.longitude}&date={date}'
res = requests.get(api_url)
if res.status_code == 200:
data = res.json()
results = data.get('results', {})
if results.get('sunrise') and results.get('sunset'):
sun_times.append({
"date": date,
"visit_id": visit.get('id'),
"sunrise": results.get('sunrise'),
"sunset": results.get('sunset')
})
# # Convert the adventure instance within an atomic transaction. response_data['sun_times'] = sun_times
# with transaction.atomic(): return Response(response_data)
# if target_type == "transportation":
# new_instance = Transportation.objects.create(**conversion_data)
# serializer = TransportationSerializer(new_instance)
# else: # target_type == "lodging"
# new_instance = Lodging.objects.create(**conversion_data)
# serializer = LodgingSerializer(new_instance)
# # Optionally, delete the original adventure to avoid duplicates.
# adventure.delete()
# return Response(serializer.data)

View file

@ -44,6 +44,15 @@ export type Adventure = {
user?: User | null; user?: User | null;
}; };
export type AdditionalAdventure = Adventure & {
sun_times: {
date: string;
visit_id: string;
sunrise: string;
sunset: string;
}[];
};
export type Country = { export type Country = {
id: number; id: number;
name: string; name: string;

View file

@ -90,6 +90,8 @@
"visits": "Visits", "visits": "Visits",
"create_new": "Create New...", "create_new": "Create New...",
"adventure": "Adventure", "adventure": "Adventure",
"additional_info": "Additional Information",
"sunrise_sunset": "Sunrise & Sunset",
"count_txt": "results matching your search", "count_txt": "results matching your search",
"sort": "Sort", "sort": "Sort",
"order_by": "Order By", "order_by": "Order By",

View file

@ -1,11 +1,11 @@
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
import type { Adventure, Collection } from '$lib/types'; import type { AdditionalAdventure, Adventure, Collection } from '$lib/types';
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
export const load = (async (event) => { export const load = (async (event) => {
const id = event.params as { id: string }; const id = event.params as { id: string };
let request = await fetch(`${endpoint}/api/adventures/${id.id}/`, { let request = await fetch(`${endpoint}/api/adventures/${id.id}/additional-info/`, {
headers: { headers: {
Cookie: `sessionid=${event.cookies.get('sessionid')}` Cookie: `sessionid=${event.cookies.get('sessionid')}`
}, },
@ -19,7 +19,7 @@ export const load = (async (event) => {
} }
}; };
} else { } else {
let adventure = (await request.json()) as Adventure; let adventure = (await request.json()) as AdditionalAdventure;
let collection: Collection | null = null; let collection: Collection | null = null;
if (adventure.collection) { if (adventure.collection) {

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { Adventure } from '$lib/types'; import type { AdditionalAdventure, Adventure } from '$lib/types';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
@ -12,6 +12,7 @@
import toGeoJSON from '@mapbox/togeojson'; import toGeoJSON from '@mapbox/togeojson';
import LightbulbOn from '~icons/mdi/lightbulb-on'; import LightbulbOn from '~icons/mdi/lightbulb-on';
import WeatherSunset from '~icons/mdi/weather-sunset';
let geojson: any; let geojson: any;
@ -75,7 +76,7 @@
export let data: PageData; export let data: PageData;
console.log(data); console.log(data);
let adventure: Adventure; let adventure: AdditionalAdventure;
let currentSlide = 0; let currentSlide = 0;
@ -112,7 +113,7 @@
await getGpxFiles(); await getGpxFiles();
}); });
async function saveEdit(event: CustomEvent<Adventure>) { async function saveEdit(event: CustomEvent<AdditionalAdventure>) {
adventure = event.detail; adventure = event.detail;
isEditModalOpen = false; isEditModalOpen = false;
geojson = null; geojson = null;
@ -522,60 +523,108 @@
</MapLibre> </MapLibre>
{/if} {/if}
</div> </div>
{#if adventure.attachments && adventure.attachments.length > 0}
<div>
<!-- attachments -->
<h2 class="text-2xl font-bold mt-4">
{$t('adventures.attachments')}
<div class="tooltip z-10" data-tip={$t('adventures.gpx_tip')}>
<button class="btn btn-sm btn-circle btn-neutral">
<LightbulbOn class="w-6 h-6" />
</button>
</div>
</h2>
<div class="grid gap-4 mt-4"> <!-- Additional Info Display Section -->
{#if adventure.attachments && adventure.attachments.length > 0}
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"> <div>
{#each adventure.attachments as attachment} {#if adventure.sun_times && adventure.sun_times.length > 0}
<AttachmentCard {attachment} /> <h2 class="text-2xl font-bold mt-4 mb-4">{$t('adventures.additional_info')}</h2>
{/each} {#if adventure.sun_times && adventure.sun_times.length > 0}
<div class="collapse collapse-plus bg-base-200 mb-2 overflow-visible">
<input type="checkbox" />
<div class="collapse-title text-xl font-medium">
<span>
{$t('adventures.sunrise_sunset')}
<WeatherSunset class="w-6 h-6 inline-block ml-2 -mt-1" />
</span>
</div> </div>
{/if}
</div> <div class="collapse-content">
</div> <div class="grid gap-4 mt-4">
{/if} <!-- Sunrise and Sunset times -->
{#if adventure.images && adventure.images.length > 0} {#each adventure.sun_times as sun_time}
<div> <div class="grid md:grid-cols-3 gap-4">
<h2 class="text-2xl font-bold mt-4">{$t('adventures.images')}</h2> <div>
<div class="grid gap-4 mt-4"> <p class="text-sm text-muted-foreground">Date</p>
{#if adventure.images && adventure.images.length > 0} <p class="text-base font-medium">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> {new Date(sun_time.date).toLocaleDateString()}
{#each adventure.images as image} </p>
<div class="relative">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-missing-attribute -->
<!-- svelte-ignore a11y-missing-content -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="w-full h-48 bg-cover bg-center rounded-lg"
style="background-image: url({image.image})"
on:click={() => (image_url = image.image)}
></div>
{#if image.is_primary}
<div
class="absolute top-0 right-0 bg-primary text-white px-2 py-1 rounded-bl-lg"
>
{$t('adventures.primary')}
</div> </div>
{/if} <div>
</div> <p class="text-sm text-muted-foreground">Sunrise</p>
{/each} <p class="text-base font-medium">
{sun_time.sunrise}
</p>
</div>
<div>
<p class="text-sm text-muted-foreground">Sunset</p>
<p class="text-base font-medium">
{sun_time.sunset}
</p>
</div>
</div>
{/each}
</div>
</div> </div>
{/if} </div>
{/if}
{/if}
{#if adventure.attachments && adventure.attachments.length > 0}
<div>
<!-- attachments -->
<h2 class="text-2xl font-bold mt-4">
{$t('adventures.attachments')}
<div class="tooltip z-10" data-tip={$t('adventures.gpx_tip')}>
<button class="btn btn-sm btn-circle btn-neutral">
<LightbulbOn class="w-6 h-6" />
</button>
</div>
</h2>
<div class="grid gap-4 mt-4">
{#if adventure.attachments && adventure.attachments.length > 0}
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{#each adventure.attachments as attachment}
<AttachmentCard {attachment} />
{/each}
</div>
{/if}
</div>
</div> </div>
</div> {/if}
{/if} {#if adventure.images && adventure.images.length > 0}
<div>
<h2 class="text-2xl font-bold mt-4">{$t('adventures.images')}</h2>
<div class="grid gap-4 mt-4">
{#if adventure.images && adventure.images.length > 0}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{#each adventure.images as image}
<div class="relative">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-missing-attribute -->
<!-- svelte-ignore a11y-missing-content -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="w-full h-48 bg-cover bg-center rounded-lg"
style="background-image: url({image.image})"
on:click={() => (image_url = image.image)}
></div>
{#if image.is_primary}
<div
class="absolute top-0 right-0 bg-primary text-white px-2 py-1 rounded-bl-lg"
>
{$t('adventures.primary')}
</div>
{/if}
</div>
{/each}
</div>
{/if}
</div>
</div>
{/if}
</div>
</div> </div>
</div> </div>
</div> </div>