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

Add StravaActivity and Activity types to types.ts

- Introduced StravaActivity type to represent detailed activity data from Strava.
- Added Activity type to encapsulate user activities, including optional trail and GPX file information.
- Updated Location type to include an array of activities associated with each visit.
This commit is contained in:
Sean Morley 2025-08-03 13:41:57 -04:00
parent c7ff8f4bc7
commit 5046bd49f7
13 changed files with 1626 additions and 255 deletions

View file

@ -1,8 +1,8 @@
import os
from django.contrib import admin
from django.utils.html import mark_safe
from .models import Location, Checklist, ChecklistItem, Collection, Transportation, Note, ContentImage, Visit, Category, ContentAttachment, Lodging, CollectionInvite, Trail
from worldtravel.models import Country, Region, VisitedRegion, City, VisitedCity
from .models import Location, Checklist, ChecklistItem, Collection, Transportation, Note, ContentImage, Visit, Category, ContentAttachment, Lodging, CollectionInvite, Trail, Activity
from worldtravel.models import Country, Region, VisitedRegion, City, VisitedCity
from allauth.account.decorators import secure_admin_login
admin.autodiscover()
@ -165,6 +165,7 @@ admin.site.register(ContentAttachment)
admin.site.register(Lodging)
admin.site.register(CollectionInvite, CollectionInviteAdmin)
admin.site.register(Trail)
admin.site.register(Activity)
admin.site.site_header = 'AdventureLog Admin'
admin.site.site_title = 'AdventureLog Admin Site'

File diff suppressed because one or more lines are too long

View file

@ -618,4 +618,59 @@ class Trail(models.Model):
super().save(*args, **kwargs)
def __str__(self):
return f"{self.name} ({'Wanderer' if self.wanderer_id else 'External'})"
return f"{self.name} ({'Wanderer' if self.wanderer_id else 'External'})"
class Activity(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
user = models.ForeignKey(User, on_delete=models.CASCADE, default=default_user)
visit = models.ForeignKey(Visit, on_delete=models.CASCADE, related_name='activities')
trail = models.ForeignKey(Trail, on_delete=models.CASCADE, related_name='activities', blank=True, null=True)
# GPX File
gpx_file = models.FileField(upload_to=PathAndRename('activities/'), validators=[validate_file_extension], blank=True, null=True)
# Descriptive
name = models.CharField(max_length=200)
type = models.CharField(max_length=100, default='general') # E.g., Run, Hike, Bike
sport_type = models.CharField(max_length=100, blank=True, null=True) # Optional detailed type
# Time & Distance
distance = models.FloatField(blank=True, null=True) # in meters
moving_time = models.DurationField(blank=True, null=True)
elapsed_time = models.DurationField(blank=True, null=True)
rest_time = models.DurationField(blank=True, null=True)
# Elevation
elevation_gain = models.FloatField(blank=True, null=True) # in meters
elevation_loss = models.FloatField(blank=True, null=True) # estimated
elev_high = models.FloatField(blank=True, null=True)
elev_low = models.FloatField(blank=True, null=True)
# Timing
start_date = models.DateTimeField(blank=True, null=True)
start_date_local = models.DateTimeField(blank=True, null=True)
timezone = models.CharField(max_length=50, choices=[(tz, tz) for tz in TIMEZONES], blank=True, null=True)
# Speed
average_speed = models.FloatField(blank=True, null=True) # in m/s
max_speed = models.FloatField(blank=True, null=True) # in m/s
# Optional metrics
average_cadence = models.FloatField(blank=True, null=True)
calories = models.FloatField(blank=True, null=True)
# Coordinates
start_lat = models.FloatField(blank=True, null=True)
start_lng = models.FloatField(blank=True, null=True)
end_lat = models.FloatField(blank=True, null=True)
end_lng = models.FloatField(blank=True, null=True)
# Optional links
external_service_id = models.CharField(max_length=100, blank=True, null=True) # E.g., Strava ID
def __str__(self):
return f"{self.name} ({self.type})"
class Meta:
verbose_name = "Activity"
verbose_name_plural = "Activities"

View file

@ -111,7 +111,12 @@ class IsOwnerOrSharedWithFullAccess(permissions.BasePermission):
# 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 type(obj).__name__ == 'Trail':
obj = obj.location
if type(obj).__name__ == 'Activity':
# If the object is an Activity, get its location
if hasattr(obj, 'visit') and hasattr(obj.visit, 'location'):
obj = obj.visit.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

@ -1,6 +1,6 @@
from django.utils import timezone
import os
from .models import Location, ContentImage, ChecklistItem, Collection, Note, Transportation, Checklist, Visit, Category, ContentAttachment, Lodging, CollectionInvite, Trail
from .models import Location, ContentImage, ChecklistItem, Collection, Note, Transportation, Checklist, Visit, Category, ContentAttachment, Lodging, CollectionInvite, Trail, Activity
from rest_framework import serializers
from main.utils import CustomModelSerializer
from users.serializers import CustomUserDetailsSerializer
@ -106,11 +106,34 @@ class TrailSerializer(CustomModelSerializer):
return 'Outdooractive'
return 'External Link'
class ActivitySerializer(CustomModelSerializer):
trail = TrailSerializer(read_only=True)
class Meta:
model = Activity
fields = [
'id', 'user', 'visit', 'trail', 'gpx_file', 'name', 'type', 'sport_type',
'distance', 'moving_time', 'elapsed_time', 'rest_time', 'elevation_gain',
'elevation_loss', 'elev_high', 'elev_low', 'start_date', 'start_date_local',
'timezone', 'average_speed', 'max_speed', 'average_cadence', 'calories',
'start_lat', 'start_lng', 'end_lat', 'end_lng', 'external_service_id'
]
read_only_fields = ['id', 'user']
def to_representation(self, instance):
representation = super().to_representation(instance)
if instance.gpx_file:
public_url = os.environ.get('PUBLIC_URL', 'http://127.0.0.1:8000').rstrip('/').replace("'", "")
representation['gpx_file'] = f"{public_url}/media/{instance.gpx_file.name}"
return representation
class VisitSerializer(serializers.ModelSerializer):
activities = ActivitySerializer(many=True, read_only=True, required=False)
class Meta:
model = Visit
fields = ['id', 'start_date', 'end_date', 'timezone', 'notes']
fields = ['id', 'start_date', 'end_date', 'timezone', 'notes', 'activities']
read_only_fields = ['id']
class LocationSerializer(CustomModelSerializer):

View file

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

View file

@ -15,4 +15,5 @@ from .attachment_view import *
from .lodging_view import *
from .recommendations_view import *
from .import_export_view import *
from .trail_view import *
from .trail_view import *
from .activity_view import *

View file

@ -0,0 +1,40 @@
from rest_framework import viewsets
from django.db.models import Q
from adventures.models import Location, Activity
from adventures.serializers import ActivitySerializer
from adventures.permissions import IsOwnerOrSharedWithFullAccess
class ActivityViewSet(viewsets.ModelViewSet):
serializer_class = ActivitySerializer
permission_classes = [IsOwnerOrSharedWithFullAccess]
def get_queryset(self):
"""
Returns activities based on location permissions.
Users can only see activities 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 Activity.objects.none()
# Build the filter for accessible locations
location_filter = Q(visit__location__user=user) # User owns the location
# Location is in collections (many-to-many) that are shared with user
location_filter |= Q(visit__location__collections__shared_with=user)
# Location is in collections (many-to-many) that user owns
location_filter |= Q(visit__location__collections__user=user)
return Activity.objects.filter(location_filter).distinct()
def perform_create(self, serializer):
"""
Set the user when creating an activity.
"""
serializer.save(user=self.request.user)