Bug Fixes + Duplicate Support (#1016)

* Update README.md supporter list

* Fix: Multiple bug fixes and features bundle (#888, #991, #617, #984) (#1007)

* fix: resolve location creation failures, broken image uploads, and invalid URL handling

- Add missing addToast import in LocationDetails.svelte for proper error feedback
- Add objectId check and error response handling in ImageManagement.svelte to prevent ghost images
- Add Content-Type check in +page.server.ts image action to handle non-JSON backend responses
- Add client-side URL validation in LocationDetails.svelte (invalid URLs → null)
- Improve Django field error extraction for user-friendly toast messages
- Clean up empty description fields (whitespace → null)
- Update BUGFIX_DOCUMENTATION.md with detailed fix descriptions

* feat: bug fixes and new features bundle

Bug fixes:
- fix: resolve PATCH location with visits (#888)
- fix: Wikipedia/URL image upload via server-side proxy (#991)
- fix: private/public toggle race condition (#617)
- fix: location creation feedback (addToast import)
- fix: invalid URL handling for locations and collections
- fix: world map country highlighting (bg-*-200 -> bg-*-400)
- fix: clipboard API polyfill for HTTP contexts
- fix: MultipleObjectsReturned for duplicate images
- fix: SvelteKit proxy sessionid cookie forwarding

Features:
- feat: duplicate location button (list + detail view)
- feat: duplicate collection button
- feat: i18n translations for 19 languages
- feat: improved error handling and user feedback

Technical:
- Backend: fetch_from_url endpoint with SSRF protection
- Backend: validate_link() for collections
- Backend: file_permissions filter() instead of get()
- Frontend: copyToClipboard() helper function
- Frontend: clipboard polyfill via server-side injection

* chore: switch docker-compose from image to build

Use local source code builds instead of upstream :latest images
to preserve our custom patches and fixes.

* fix: lodging save errors, AI language support, and i18n improvements

- Fix Lodging save: add res.ok checks, error toasts, isSaving state (#984)
- Fix URL validation: silently set invalid URLs to null (Lodging, Transportation)
- Fix AI description language: pass user locale to Wikipedia API
- Fix missing i18n keys: Strava toggle buttons (show/hide)
- Add CHANGELOG.md
- Remove internal documentation from public tracking
- Update .gitignore for Cursor IDE and internal docs

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: update location duplication handling, improve UI feedback, and enhance localization support

---------

Co-authored-by: AdventureLog Bugfix <bugfix@adventurelog.local>
Co-authored-by: madmp87 <info@so-pa.de>
Co-authored-by: Mathias Ponnwitz <devuser@dockge-dev.fritz.box>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Sean Morley <mail@seanmorley.com>

* Enhance duplication functionality for collections and locations; update UI to reflect changes

* Potential fix for code scanning alert no. 49: Information exposure through an exception

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* Update Django and Pillow versions in requirements.txt

* Fix error logging for image fetch timeout in ContentImageViewSet

* Update requirements.txt to include jaraco.context and wheel for security fixes

* Update app version and add security vulnerabilities to .trivyignore

* Update backend/server/adventures/views/collection_view.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update frontend/src/lib/types.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Reorder build and image directives in docker-compose.yml for clarity

* Refactor code structure for improved readability and maintainability

* Remove inline clipboard polyfill script injection from server hooks (#1019)

* Initial plan

* Remove inline clipboard polyfill script injection from hooks.server.ts

Co-authored-by: seanmorley15 <98704938+seanmorley15@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: seanmorley15 <98704938+seanmorley15@users.noreply.github.com>

* Fix unhandled promise rejections in copyToClipboard click handlers (#1018)

* Initial plan

* Fix: make copyToClipboard handlers async with try/catch error toast

Co-authored-by: seanmorley15 <98704938+seanmorley15@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: seanmorley15 <98704938+seanmorley15@users.noreply.github.com>

* Harden `fetch_from_url` image proxy: require auth, rate-limit, and strengthen SSRF protections (#1017)

* Initial plan

* Harden fetch_from_url: require auth, rate-limit, block non-standard ports, check all IPs, re-validate redirects

Co-authored-by: seanmorley15 <98704938+seanmorley15@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: seanmorley15 <98704938+seanmorley15@users.noreply.github.com>

* Fix subregion filtering in world travel page to exclude null values

* Update package.json to use caret (^) for versioning in overrides

* fix: update package dependencies for compatibility and stability

- Added cookie dependency with version constraint <0.7.0
- Updated svelte dependency to allow versions <=5.51.4
- Updated @sveltejs/adapter-vercel dependency to allow versions <6.3.2

* Refactor code structure for improved readability and maintainability

---------

Co-authored-by: madmp87 <79420509+madmp87@users.noreply.github.com>
Co-authored-by: AdventureLog Bugfix <bugfix@adventurelog.local>
Co-authored-by: madmp87 <info@so-pa.de>
Co-authored-by: Mathias Ponnwitz <devuser@dockge-dev.fritz.box>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
Sean Morley
2026-02-20 20:49:24 -05:00
committed by GitHub
parent c008f0c264
commit bec90fe2a5
57 changed files with 21743 additions and 20304 deletions

View File

@@ -1,18 +1,22 @@
import logging
from django.utils import timezone
from django.db import transaction
from django.core.exceptions import PermissionDenied
from django.core.files.base import ContentFile
from django.db.models import Q, Max, Prefetch
from django.db.models.functions import Lower
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
import requests
from adventures.models import Location, Category, CollectionItineraryItem, Visit
from adventures.models import Location, Category, Collection, CollectionItineraryItem, ContentImage, Visit
from django.contrib.contenttypes.models import ContentType
from adventures.permissions import IsOwnerOrSharedWithFullAccess
from adventures.serializers import LocationSerializer, MapPinSerializer, CalendarLocationSerializer
from adventures.utils import pagination
logger = logging.getLogger(__name__)
class LocationViewSet(viewsets.ModelViewSet):
"""
ViewSet for managing Adventure objects with support for filtering, sorting,
@@ -254,6 +258,131 @@ class LocationViewSet(viewsets.ModelViewSet):
return Response(response_data)
@action(detail=True, methods=['post'])
def duplicate(self, request, pk=None):
"""Create a duplicate of an existing location.
Copies all fields except collections and visits. Images are duplicated as
independent files (not shared references). The name is prefixed with
"Copy of " and is_public is reset to False.
"""
original = self.get_object()
# Verify the requesting user owns the location or has access
if not self._has_adventure_access(original, request.user):
return Response(
{"error": "You do not have permission to duplicate this location."},
status=status.HTTP_403_FORBIDDEN,
)
try:
with transaction.atomic():
target_collection = None
target_collection_id = request.data.get('collection_id')
if target_collection_id:
try:
target_collection = Collection.objects.get(id=target_collection_id)
except Collection.DoesNotExist:
return Response(
{"error": "Collection not found."},
status=status.HTTP_404_NOT_FOUND,
)
user_can_link_to_collection = (
target_collection.user == request.user
or target_collection.shared_with.filter(uuid=request.user.uuid).exists()
)
if not user_can_link_to_collection:
return Response(
{"error": "You do not have permission to add locations to this collection."},
status=status.HTTP_403_FORBIDDEN,
)
# Snapshot original images before creating the copy
original_images = list(original.images.all())
# Build the new location
new_location = Location(
user=request.user,
name=f"Copy of {original.name}",
description=original.description,
rating=original.rating,
link=original.link,
location=original.location,
tags=list(original.tags) if original.tags else None,
is_public=False,
longitude=original.longitude,
latitude=original.latitude,
city=original.city,
region=original.region,
country=original.country,
price=original.price,
price_currency=original.price_currency,
)
# Handle category: reuse the user's own matching category or
# create one if necessary.
if original.category:
category, _ = Category.objects.get_or_create(
user=request.user,
name=original.category.name,
defaults={
'display_name': original.category.display_name,
'icon': original.category.icon,
},
)
new_location.category = category
new_location.save()
# If requested, link the duplicate only to the current collection.
# This avoids accidentally inheriting all source collections.
if target_collection:
new_location.collections.set([target_collection])
# Duplicate images as independent files/new records
location_ct = ContentType.objects.get_for_model(Location)
for img in original_images:
if img.image:
try:
img.image.open('rb')
image_bytes = img.image.read()
finally:
try:
img.image.close()
except Exception:
pass
file_name = (img.image.name or '').split('/')[-1] or 'image.webp'
ContentImage.objects.create(
content_type=location_ct,
object_id=str(new_location.id),
image=ContentFile(image_bytes, name=file_name),
immich_id=None,
is_primary=img.is_primary,
user=request.user,
)
else:
ContentImage.objects.create(
content_type=location_ct,
object_id=str(new_location.id),
immich_id=img.immich_id,
is_primary=img.is_primary,
user=request.user,
)
serializer = self.get_serializer(new_location)
return Response(serializer.data, status=status.HTTP_201_CREATED)
except Exception:
logger.exception("Failed to duplicate location %s", pk)
return Response(
{"error": "An error occurred while duplicating the location."},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
# view to return location name and lat/lon for all locations a user owns for the golobal map
@action(detail=False, methods=['get'], url_path='pins')
def map_locations(self, request):