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
# 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':
obj = obj.location
@ -117,6 +120,13 @@ class IsOwnerOrSharedWithFullAccess(permissions.BasePermission):
if hasattr(obj, 'visit') and hasattr(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
if not user or not user.is_authenticated:
return is_safe_method and getattr(obj, 'is_public', False)

View file

@ -215,8 +215,13 @@ class VisitSerializer(serializers.ModelSerializer):
class Meta:
model = Visit
fields = ['id', 'start_date', 'end_date', 'timezone', 'notes', 'activities']
read_only_fields = ['id']
fields = ['id', 'start_date', 'end_date', 'timezone', 'notes', 'activities','location', 'created_at', 'updated_at']
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):
images = serializers.SerializerMethodField()
@ -358,17 +363,12 @@ class LocationSerializer(CustomModelSerializer):
return obj.is_visited_status()
def create(self, validated_data):
visits_data = validated_data.pop('visits', [])
category_data = validated_data.pop('category', None)
collections_data = validated_data.pop('collections', [])
print(category_data)
location = Location.objects.create(**validated_data)
# Handle visits
for visit_data in visits_data:
Visit.objects.create(location=location, **visit_data)
# Handle category
if category_data:
category = self.get_or_create_category(category_data)
@ -384,7 +384,6 @@ class LocationSerializer(CustomModelSerializer):
def update(self, instance, validated_data):
has_visits = 'visits' in validated_data
visits_data = validated_data.pop('visits', [])
category_data = validated_data.pop('category', None)
collections_data = validated_data.pop('collections', None)
@ -405,27 +404,6 @@ class LocationSerializer(CustomModelSerializer):
if collections_data is not None:
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
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'trails', TrailViewSet, basename='trails')
router.register(r'activities', ActivityViewSet, basename='activities')
router.register(r'visits', VisitViewSet, basename='visits')
urlpatterns = [
# Include the router under the 'api/' prefix

View file

@ -17,3 +17,4 @@ from .recommendations_view import *
from .import_export_view import *
from .trail_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.serializers import ActivitySerializer
from adventures.permissions import IsOwnerOrSharedWithFullAccess
from rest_framework.exceptions import PermissionDenied
class ActivityViewSet(viewsets.ModelViewSet):
serializer_class = ActivitySerializer
@ -37,4 +38,32 @@ class ActivityViewSet(viewsets.ModelViewSet):
"""
Set the user when creating an activity.
"""
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.serializers import TrailSerializer
from adventures.permissions import IsOwnerOrSharedWithFullAccess
from rest_framework.exceptions import PermissionDenied
class TrailViewSet(viewsets.ModelViewSet):
serializer_class = TrailSerializer
@ -34,7 +35,32 @@ class TrailViewSet(viewsets.ModelViewSet):
return Trail.objects.filter(location_filter).distinct()
def perform_create(self, serializer):
"""
Set the user when creating a trail.
"""
location = serializer.validated_data.get('location')
# Optional: import this if not in the same file
# 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() {
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);
}

File diff suppressed because it is too large Load diff

View file

@ -298,7 +298,6 @@
bind:visits={location.visits}
bind:trails={location.trails}
objectId={location.id}
type="location"
on:back={() => {
steps[3].selected = false;
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;
link?: string | null;
images: ContentImage[];
visits: {
id: string;
start_date: string;
end_date: string;
timezone: string | null;
notes: string;
activities: Activity[]; // Array of activities associated with the visit
}[];
visits: Visit[];
collections?: string[] | null;
latitude: number | null;
longitude: number | null;
@ -425,6 +418,9 @@ export type Visit = {
notes: string;
timezone: string | null;
activities?: Activity[];
location: string;
created_at: string;
updated_at: string;
};
export type TransportationVisit = {