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
|
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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 *
|
|
@ -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()
|
|
@ -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()
|
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() {
|
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
|
@ -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
|
@ -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 = {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue