Skip to content

Commit

Permalink
Merge pull request #146 from seanmorley15/development
Browse files Browse the repository at this point in the history
Trip Planner for Collections
  • Loading branch information
seanmorley15 authored Jul 28, 2024
2 parents 9221e4b + 589eb09 commit 03ca077
Show file tree
Hide file tree
Showing 26 changed files with 1,273 additions and 121 deletions.
3 changes: 2 additions & 1 deletion backend/server/adventures/admin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
from django.contrib import admin
from django.utils.html import mark_safe
from .models import Adventure, Collection
from .models import Adventure, Collection, Transportation
from worldtravel.models import Country, Region, VisitedRegion


Expand Down Expand Up @@ -74,6 +74,7 @@ def adventure_count(self, obj):
admin.site.register(Country, CountryAdmin)
admin.site.register(Region, RegionAdmin)
admin.site.register(VisitedRegion)
admin.site.register(Transportation)

admin.site.site_header = 'AdventureLog Admin'
admin.site.site_title = 'AdventureLog Admin Site'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 5.0.7 on 2024-07-27 18:18

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('adventures', '0011_adventure_updated_at'),
]

operations = [
migrations.AddField(
model_name='collection',
name='end_date',
field=models.DateField(blank=True, null=True),
),
migrations.AddField(
model_name='collection',
name='start_date',
field=models.DateField(blank=True, null=True),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Generated by Django 5.0.7 on 2024-07-27 22:49

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('adventures', '0012_collection_end_date_collection_start_date'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.AlterField(
model_name='adventure',
name='type',
field=models.CharField(choices=[('visited', 'Visited'), ('planned', 'Planned'), ('lodging', 'Lodging'), ('dining', 'Dining')], max_length=100),
),
migrations.CreateModel(
name='Transportation',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('type', models.CharField(max_length=100)),
('name', models.CharField(max_length=200)),
('description', models.TextField(blank=True, null=True)),
('rating', models.FloatField(blank=True, null=True)),
('link', models.URLField(blank=True, null=True)),
('date', models.DateTimeField(blank=True, null=True)),
('flight_number', models.CharField(blank=True, max_length=100, null=True)),
('from_location', models.CharField(blank=True, max_length=200, null=True)),
('to_location', models.CharField(blank=True, max_length=200, null=True)),
('is_public', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('collection', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='adventures.collection')),
('user_id', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]
45 changes: 45 additions & 0 deletions backend/server/adventures/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@
ADVENTURE_TYPES = [
('visited', 'Visited'),
('planned', 'Planned'),
('lodging', 'Lodging'),
('dining', 'Dining')
]

TRANSPORTATION_TYPES = [
('car', 'Car'),
('plane', 'Plane'),
('train', 'Train'),
('bus', 'Bus'),
('boat', 'Boat'),
('bike', 'Bike'),
('walking', 'Walking'),
('other', 'Other')
]


Expand Down Expand Up @@ -56,6 +69,8 @@ class Collection(models.Model):
description = models.TextField(blank=True, null=True)
is_public = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
start_date = models.DateField(blank=True, null=True)
end_date = models.DateField(blank=True, null=True)

# if connected adventures are private and collection is public, raise an error
def clean(self):
Expand All @@ -66,3 +81,33 @@ def clean(self):

def __str__(self):
return self.name

# make a class for transportaiotn and make it linked to a collection. Make it so it can be used for different types of transportations like car, plane, train, etc.

class Transportation(models.Model):
id = models.AutoField(primary_key=True)
user_id = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user_id)
type = models.CharField(max_length=100, choices=TRANSPORTATION_TYPES)
name = models.CharField(max_length=200)
description = models.TextField(blank=True, null=True)
rating = models.FloatField(blank=True, null=True)
link = models.URLField(blank=True, null=True)
date = models.DateTimeField(blank=True, null=True)
flight_number = models.CharField(max_length=100, blank=True, null=True)
from_location = models.CharField(max_length=200, blank=True, null=True)
to_location = models.CharField(max_length=200, blank=True, null=True)
is_public = models.BooleanField(default=False)
collection = models.ForeignKey('Collection', on_delete=models.CASCADE, blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

def clean(self):
if self.collection:
if self.collection.is_public and not self.is_public:
raise ValidationError('Transportations associated with a public collection must be public. Collection: ' + self.collection.name + ' Transportation: ' + self.name)
if self.user_id != self.collection.user_id:
raise ValidationError('Transportations must be associated with collections owned by the same user. Collection owner: ' + self.collection.user_id.username + ' Transportation owner: ' + self.user_id.username)

def __str__(self):
return self.name
40 changes: 38 additions & 2 deletions backend/server/adventures/serializers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os
from .models import Adventure, Collection
from .models import Adventure, Collection, Transportation
from rest_framework import serializers

class AdventureSerializer(serializers.ModelSerializer):
Expand All @@ -23,13 +23,49 @@ def validate_activity_types(self, value):
return [activity.lower() for activity in value]
return value

class TransportationSerializer(serializers.ModelSerializer):

class Meta:
model = Transportation
fields = [
'id', 'user_id', 'type', 'name', 'description', 'rating',
'link', 'date', 'flight_number', 'from_location', 'to_location',
'is_public', 'collection', 'created_at', 'updated_at'
]
read_only_fields = ['id', 'created_at', 'updated_at']

def validate(self, data):
# Check if the collection is public and the transportation is not
collection = data.get('collection')
is_public = data.get('is_public', False)
if collection and collection.is_public and not is_public:
raise serializers.ValidationError(
'Transportations associated with a public collection must be public.'
)

# Check if the user owns the collection
request = self.context.get('request')
if request and collection and collection.user_id != request.user:
raise serializers.ValidationError(
'Transportations must be associated with collections owned by the same user.'
)

return data

def create(self, validated_data):
# Set the user_id to the current user
validated_data['user_id'] = self.context['request'].user
return super().create(validated_data)


class CollectionSerializer(serializers.ModelSerializer):
adventures = AdventureSerializer(many=True, read_only=True, source='adventure_set')
transportations = TransportationSerializer(many=True, read_only=True, source='transportation_set')

class Meta:
model = Collection
# fields are all plus the adventures field
fields = ['id', 'description', 'user_id', 'name', 'is_public', 'adventures']
fields = ['id', 'description', 'user_id', 'name', 'is_public', 'adventures', 'created_at', 'start_date', 'end_date', 'transportations']



3 changes: 2 additions & 1 deletion backend/server/adventures/urls.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from .views import AdventureViewSet, CollectionViewSet, StatsViewSet, GenerateDescription, ActivityTypesView
from .views import AdventureViewSet, CollectionViewSet, StatsViewSet, GenerateDescription, ActivityTypesView, TransportationViewSet

router = DefaultRouter()
router.register(r'adventures', AdventureViewSet, basename='adventures')
router.register(r'collections', CollectionViewSet, basename='collections')
router.register(r'stats', StatsViewSet, basename='stats')
router.register(r'generate', GenerateDescription, basename='generate')
router.register(r'activity-types', ActivityTypesView, basename='activity-types')
router.register(r'transportations', TransportationViewSet, basename='transportations')


urlpatterns = [
Expand Down
71 changes: 48 additions & 23 deletions backend/server/adventures/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
from rest_framework import viewsets
from django.db.models.functions import Lower
from rest_framework.response import Response
from .models import Adventure, Collection
from .models import Adventure, Collection, Transportation
from worldtravel.models import VisitedRegion, Region, Country
from .serializers import AdventureSerializer, CollectionSerializer
from .serializers import AdventureSerializer, CollectionSerializer, TransportationSerializer
from rest_framework.permissions import IsAuthenticated
from django.db.models import Q, Prefetch
from .permissions import IsOwnerOrReadOnly, IsPublicReadOnly
Expand Down Expand Up @@ -138,8 +138,9 @@ def all(self, request):
# queryset = Adventure.objects.filter(
# Q(is_public=True) | Q(user_id=request.user.id), collection=None
# )
allowed_types = ['visited', 'planned']
queryset = Adventure.objects.filter(
Q(user_id=request.user.id)
Q(user_id=request.user.id) & Q(type__in=allowed_types)
)

queryset = self.apply_sorting(queryset)
Expand Down Expand Up @@ -237,6 +238,9 @@ def update(self, request, *args, **kwargs):
# Update associated adventures to match the collection's is_public status
Adventure.objects.filter(collection=instance).update(is_public=new_public_status)

# do the same for transportations
Transportation.objects.filter(collection=instance).update(is_public=new_public_status)

# Log the action (optional)
action = "public" if new_public_status else "private"
print(f"Collection {instance.id} and its adventures were set to {action}")
Expand All @@ -257,31 +261,15 @@ def get_queryset(self):
Prefetch('adventure_set', queryset=Adventure.objects.filter(
Q(is_public=True) | Q(user_id=self.request.user.id)
))
).prefetch_related(
Prefetch('transportation_set', queryset=Transportation.objects.filter(
Q(is_public=True) | Q(user_id=self.request.user.id)
))
)
return self.apply_sorting(collections)

def perform_create(self, serializer):
serializer.save(user_id=self.request.user)

# @action(detail=False, methods=['get'])
# def filtered(self, request):
# types = request.query_params.get('types', '').split(',')
# valid_types = ['visited', 'planned']
# types = [t for t in types if t in valid_types]

# if not types:
# return Response({"error": "No valid types provided"}, status=400)

# queryset = Collection.objects.none()

# for adventure_type in types:
# if adventure_type in ['visited', 'planned']:
# queryset |= Collection.objects.filter(
# type=adventure_type, user_id=request.user.id)

# queryset = self.apply_sorting(queryset)
# collections = self.paginate_and_respond(queryset, request)
# return collections

def paginate_and_respond(self, queryset, request):
paginator = self.pagination_class()
Expand Down Expand Up @@ -378,3 +366,40 @@ def types(self, request):
allTypes.append(x)

return Response(allTypes)

class TransportationViewSet(viewsets.ModelViewSet):
queryset = Transportation.objects.all()
serializer_class = TransportationSerializer
permission_classes = [IsAuthenticated]
filterset_fields = ['type', 'is_public', 'collection']

# return error message if user is not authenticated on the root endpoint
def list(self, request, *args, **kwargs):
# Prevent listing all adventures
return Response({"detail": "Listing all adventures is not allowed."},
status=status.HTTP_403_FORBIDDEN)

@action(detail=False, methods=['get'])
def all(self, request):
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=400)
queryset = Transportation.objects.filter(
Q(user_id=request.user.id)
)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)


def get_queryset(self):

"""
This view should return a list of all transportations
for the currently authenticated user.
"""
user = self.request.user
return Transportation.objects.filter(user_id=user)

def perform_create(self, serializer):
serializer.save(user_id=self.request.user)


3 changes: 2 additions & 1 deletion backend/server/worldtravel/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from django.urls import include, path
from rest_framework.routers import DefaultRouter
from .views import CountryViewSet, RegionViewSet, VisitedRegionViewSet, regions_by_country, visits_by_country
from .views import CountryViewSet, RegionViewSet, VisitedRegionViewSet, regions_by_country, visits_by_country, GeoJSONView

router = DefaultRouter()
router.register(r'countries', CountryViewSet, basename='countries')
Expand All @@ -13,4 +13,5 @@
path('', include(router.urls)),
path('<str:country_code>/regions/', regions_by_country, name='regions-by-country'),
path('<str:country_code>/visits/', visits_by_country, name='visits-by-country'),
path('geojson/', GeoJSONView.as_view({'get': 'list'}), name='geojson'),
]
40 changes: 39 additions & 1 deletion backend/server/worldtravel/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
from django.shortcuts import get_object_or_404
from rest_framework.response import Response
from rest_framework.decorators import api_view, permission_classes
import os
import json
from django.conf import settings
from django.contrib.staticfiles import finders

@api_view(['GET'])
@permission_classes([IsAuthenticated])
Expand Down Expand Up @@ -49,4 +53,38 @@ def create(self, request, *args, **kwargs):
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

class GeoJSONView(viewsets.ViewSet):
"""
Combine all GeoJSON data from .json files in static/data into a single GeoJSON object.
"""
def list(self, request):
combined_geojson = {
"type": "FeatureCollection",
"features": []
}

# Use Django's static file finder to locate the 'data' directory
data_dir = finders.find('data')

if not data_dir or not os.path.isdir(data_dir):
return Response({"error": "Data directory does not exist."}, status=404)

for filename in os.listdir(data_dir):
if filename.endswith('.json'):
file_path = os.path.join(data_dir, filename)
try:
with open(file_path, 'r') as f:
json_data = json.load(f)
# Check if the JSON data is GeoJSON
if isinstance(json_data, dict) and "type" in json_data:
if json_data["type"] == "FeatureCollection":
combined_geojson["features"].extend(json_data.get("features", []))
elif json_data["type"] == "Feature":
combined_geojson["features"].append(json_data)
# You can add more conditions here for other GeoJSON types if needed
except (IOError, json.JSONDecodeError) as e:
return Response({"error": f"Error reading file {filename}: {str(e)}"}, status=500)

return Response(combined_geojson)
Loading

0 comments on commit 03ca077

Please sign in to comment.