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:
parent
b7b7f9d26d
commit
1395efa389
12 changed files with 881 additions and 2078 deletions
|
@ -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)
|
||||
|
|
|
@ -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,16 +363,11 @@ 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:
|
||||
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -16,4 +16,5 @@ from .lodging_view import *
|
|||
from .recommendations_view import *
|
||||
from .import_export_view import *
|
||||
from .trail_view import *
|
||||
from .activity_view import *
|
||||
from .activity_view import *
|
||||
from .visit_view import *
|
|
@ -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.
|
||||
"""
|
||||
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()
|
|
@ -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.
|
||||
"""
|
||||
serializer.save(user=self.request.user)
|
||||
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()
|
66
backend/server/adventures/views/visit_view.py
Normal file
66
backend/server/adventures/views/visit_view.py
Normal 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()
|
|
@ -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
|
@ -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
|
@ -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 = {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue