1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-08-08 06:35:19 +02:00

Refactor Location and Visit types: Replace visits structure in Location with Visit type and add location, created_at, and updated_at fields to Visit

This commit is contained in:
Sean Morley 2025-08-07 11:52:31 -04:00
parent b7b7f9d26d
commit 1395efa389
12 changed files with 881 additions and 2078 deletions

View file

@ -109,6 +109,9 @@ class IsOwnerOrSharedWithFullAccess(permissions.BasePermission):
is_safe_method = request.method in permissions.SAFE_METHODS is_safe_method = request.method in permissions.SAFE_METHODS
# If the object has a location field, get that location and continue checking with that object, basically from the location's perspective. I am very proud of this line of code and that's why I am writing this comment. # If the object has a location field, get that location and continue checking with that object, basically from the location's perspective. I am very proud of this line of code and that's why I am writing this comment.
print("Checking permissions for object", obj, "of type", type(obj).__name__)
if type(obj).__name__ == 'Trail': if type(obj).__name__ == 'Trail':
obj = obj.location obj = obj.location
@ -117,6 +120,13 @@ class IsOwnerOrSharedWithFullAccess(permissions.BasePermission):
if hasattr(obj, 'visit') and hasattr(obj.visit, 'location'): if hasattr(obj, 'visit') and hasattr(obj.visit, 'location'):
obj = obj.visit.location obj = obj.visit.location
if type(obj).__name__ == 'Visit':
print("Checking permissions for Visit object", obj)
# If the object is a Visit, get its location
if hasattr(obj, 'location'):
obj = obj.location
# Anonymous users only get read access to public objects # Anonymous users only get read access to public objects
if not user or not user.is_authenticated: if not user or not user.is_authenticated:
return is_safe_method and getattr(obj, 'is_public', False) return is_safe_method and getattr(obj, 'is_public', False)

View file

@ -215,8 +215,13 @@ class VisitSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Visit model = Visit
fields = ['id', 'start_date', 'end_date', 'timezone', 'notes', 'activities'] fields = ['id', 'start_date', 'end_date', 'timezone', 'notes', 'activities','location', 'created_at', 'updated_at']
read_only_fields = ['id'] read_only_fields = ['id', 'created_at', 'updated_at']
def create(self, validated_data):
if not validated_data.get('end_date') and validated_data.get('start_date'):
validated_data['end_date'] = validated_data['start_date']
return super().create(validated_data)
class LocationSerializer(CustomModelSerializer): class LocationSerializer(CustomModelSerializer):
images = serializers.SerializerMethodField() images = serializers.SerializerMethodField()
@ -358,16 +363,11 @@ class LocationSerializer(CustomModelSerializer):
return obj.is_visited_status() return obj.is_visited_status()
def create(self, validated_data): def create(self, validated_data):
visits_data = validated_data.pop('visits', [])
category_data = validated_data.pop('category', None) category_data = validated_data.pop('category', None)
collections_data = validated_data.pop('collections', []) collections_data = validated_data.pop('collections', [])
print(category_data) print(category_data)
location = Location.objects.create(**validated_data) location = Location.objects.create(**validated_data)
# Handle visits
for visit_data in visits_data:
Visit.objects.create(location=location, **visit_data)
# Handle category # Handle category
if category_data: if category_data:
@ -384,7 +384,6 @@ class LocationSerializer(CustomModelSerializer):
def update(self, instance, validated_data): def update(self, instance, validated_data):
has_visits = 'visits' in validated_data has_visits = 'visits' in validated_data
visits_data = validated_data.pop('visits', [])
category_data = validated_data.pop('category', None) category_data = validated_data.pop('category', None)
collections_data = validated_data.pop('collections', None) collections_data = validated_data.pop('collections', None)
@ -405,27 +404,6 @@ class LocationSerializer(CustomModelSerializer):
if collections_data is not None: if collections_data is not None:
instance.collections.set(collections_data) instance.collections.set(collections_data)
# Handle visits
if has_visits:
current_visits = instance.visits.all()
current_visit_ids = set(current_visits.values_list('id', flat=True))
updated_visit_ids = set()
for visit_data in visits_data:
visit_id = visit_data.get('id')
if visit_id and visit_id in current_visit_ids:
visit = current_visits.get(id=visit_id)
for attr, value in visit_data.items():
setattr(visit, attr, value)
visit.save()
updated_visit_ids.add(visit_id)
else:
new_visit = Visit.objects.create(location=instance, **visit_data)
updated_visit_ids.add(new_visit.id)
visits_to_delete = current_visit_ids - updated_visit_ids
instance.visits.filter(id__in=visits_to_delete).delete()
# call save on the location to update the updated_at field and trigger any geocoding # call save on the location to update the updated_at field and trigger any geocoding
instance.save() instance.save()

View file

@ -22,6 +22,7 @@ router.register(r'recommendations', RecommendationsViewSet, basename='recommenda
router.register(r'backup', BackupViewSet, basename='backup') router.register(r'backup', BackupViewSet, basename='backup')
router.register(r'trails', TrailViewSet, basename='trails') router.register(r'trails', TrailViewSet, basename='trails')
router.register(r'activities', ActivityViewSet, basename='activities') router.register(r'activities', ActivityViewSet, basename='activities')
router.register(r'visits', VisitViewSet, basename='visits')
urlpatterns = [ urlpatterns = [
# Include the router under the 'api/' prefix # Include the router under the 'api/' prefix

View file

@ -16,4 +16,5 @@ from .lodging_view import *
from .recommendations_view import * from .recommendations_view import *
from .import_export_view import * from .import_export_view import *
from .trail_view import * from .trail_view import *
from .activity_view import * from .activity_view import *
from .visit_view import *

View file

@ -3,6 +3,7 @@ from django.db.models import Q
from adventures.models import Location, Activity from adventures.models import Location, Activity
from adventures.serializers import ActivitySerializer from adventures.serializers import ActivitySerializer
from adventures.permissions import IsOwnerOrSharedWithFullAccess from adventures.permissions import IsOwnerOrSharedWithFullAccess
from rest_framework.exceptions import PermissionDenied
class ActivityViewSet(viewsets.ModelViewSet): class ActivityViewSet(viewsets.ModelViewSet):
serializer_class = ActivitySerializer serializer_class = ActivitySerializer
@ -37,4 +38,32 @@ class ActivityViewSet(viewsets.ModelViewSet):
""" """
Set the user when creating an activity. Set the user when creating an activity.
""" """
serializer.save(user=self.request.user) visit = serializer.validated_data.get('visit')
location = visit.location if visit else None
if location and not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, location):
raise PermissionDenied("You do not have permission to add an activity to this location.")
serializer.save(user=self.request.user)
def perform_update(self, serializer):
instance = serializer.instance
new_visit = serializer.validated_data.get('visit')
# Prevent changing visit/location after creation
if new_visit and new_visit != instance.visit:
raise PermissionDenied("Cannot change activity visit after creation. Create a new activity instead.")
# Check permission for updates to the existing location
location = instance.visit.location if instance.visit else None
if location and not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, location):
raise PermissionDenied("You do not have permission to update this activity.")
serializer.save()
def perform_destroy(self, instance):
location = instance.visit.location if instance.visit else None
if location and not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, location):
raise PermissionDenied("You do not have permission to delete this activity.")
instance.delete()

View file

@ -3,6 +3,7 @@ from django.db.models import Q
from adventures.models import Location, Trail from adventures.models import Location, Trail
from adventures.serializers import TrailSerializer from adventures.serializers import TrailSerializer
from adventures.permissions import IsOwnerOrSharedWithFullAccess from adventures.permissions import IsOwnerOrSharedWithFullAccess
from rest_framework.exceptions import PermissionDenied
class TrailViewSet(viewsets.ModelViewSet): class TrailViewSet(viewsets.ModelViewSet):
serializer_class = TrailSerializer serializer_class = TrailSerializer
@ -34,7 +35,32 @@ class TrailViewSet(viewsets.ModelViewSet):
return Trail.objects.filter(location_filter).distinct() return Trail.objects.filter(location_filter).distinct()
def perform_create(self, serializer): def perform_create(self, serializer):
""" location = serializer.validated_data.get('location')
Set the user when creating a trail.
""" # Optional: import this if not in the same file
serializer.save(user=self.request.user) # from adventures.permissions import IsOwnerOrSharedWithFullAccess
if not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, location):
raise PermissionDenied("You do not have permission to add a trail to this location.")
serializer.save(user=self.request.user)
def perform_update(self, serializer):
instance = serializer.instance
new_location = serializer.validated_data.get('location')
# Prevent changing location after creation
if new_location and new_location != instance.location:
raise PermissionDenied("Cannot change trail location after creation. Create a new trail instead.")
# Check permission for updates to the existing location
if not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, instance.location):
raise PermissionDenied("You do not have permission to update this trail.")
serializer.save()
def perform_destroy(self, instance):
if not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, instance.location):
raise PermissionDenied("You do not have permission to delete this trail.")
instance.delete()

View file

@ -0,0 +1,66 @@
from rest_framework import viewsets
from django.db.models import Q
from adventures.models import Location, Visit
from adventures.serializers import VisitSerializer
from adventures.permissions import IsOwnerOrSharedWithFullAccess
from rest_framework.exceptions import PermissionDenied
class VisitViewSet(viewsets.ModelViewSet):
serializer_class = VisitSerializer
permission_classes = [IsOwnerOrSharedWithFullAccess]
def get_queryset(self):
"""
Returns visits based on location permissions.
Users can only see visits in locations they have access to for editing/updating/deleting.
This means they are either:
- The owner of the location
- The location is in a collection that is shared with the user
- The location is in a collection that the user owns
"""
user = self.request.user
if not user or not user.is_authenticated:
return Visit.objects.none()
# Build the filter for accessible locations
location_filter = Q(location__user=user) # User owns the location
# Location is in collections (many-to-many) that are shared with user
location_filter |= Q(location__collections__shared_with=user)
# Location is in collections (many-to-many) that user owns
location_filter |= Q(location__collections__user=user)
return Visit.objects.filter(location_filter).distinct()
def perform_create(self, serializer):
"""
Set the user when creating a visit and check permissions.
"""
location = serializer.validated_data.get('location')
if not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, location):
raise PermissionDenied("You do not have permission to add a visit to this location.")
serializer.save()
def perform_update(self, serializer):
instance = serializer.instance
new_location = serializer.validated_data.get('location')
# Prevent changing location after creation
if new_location and new_location != instance.location:
raise PermissionDenied("Cannot change visit location after creation. Create a new visit instead.")
# Check permission for updates to the existing location
if not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, instance.location):
raise PermissionDenied("You do not have permission to update this visit.")
serializer.save()
def perform_destroy(self, instance):
if not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, instance.location):
raise PermissionDenied("You do not have permission to delete this visit.")
instance.delete()

View file

@ -39,6 +39,9 @@
function custom_category() { function custom_category() {
new_category.name = new_category.display_name.toLowerCase().replace(/ /g, '_'); new_category.name = new_category.display_name.toLowerCase().replace(/ /g, '_');
if (!new_category.icon) {
new_category.icon = '🌎'; // Default icon if none selected
}
selectCategory(new_category); selectCategory(new_category);
} }

File diff suppressed because it is too large Load diff

View file

@ -298,7 +298,6 @@
bind:visits={location.visits} bind:visits={location.visits}
bind:trails={location.trails} bind:trails={location.trails}
objectId={location.id} objectId={location.id}
type="location"
on:back={() => { on:back={() => {
steps[3].selected = false; steps[3].selected = false;
steps[2].selected = true; steps[2].selected = true;

File diff suppressed because it is too large Load diff

View file

@ -31,14 +31,7 @@ export type Location = {
rating?: number | null; rating?: number | null;
link?: string | null; link?: string | null;
images: ContentImage[]; images: ContentImage[];
visits: { visits: Visit[];
id: string;
start_date: string;
end_date: string;
timezone: string | null;
notes: string;
activities: Activity[]; // Array of activities associated with the visit
}[];
collections?: string[] | null; collections?: string[] | null;
latitude: number | null; latitude: number | null;
longitude: number | null; longitude: number | null;
@ -425,6 +418,9 @@ export type Visit = {
notes: string; notes: string;
timezone: string | null; timezone: string | null;
activities?: Activity[]; activities?: Activity[];
location: string;
created_at: string;
updated_at: string;
}; };
export type TransportationVisit = { export type TransportationVisit = {