mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2026-05-08 23:15:11 -04:00
* Enhance money parsing and normalization in BackupViewSet * Refactor money parsing in BackupViewSet for schema safety and enhance profile statistics display with new metrics * Improve throttling handling in auth hooks to enhance user experience during high-load scenarios * fix(deps): update countries-states-cities-database v3.1 (#1047) update countries-states-cities-database to fixed some cities error * fix: update appVersion to v0.12.0-main-031526 * feat: enhance CategoryFilterDropdown with event dispatching and URL synchronization. Fixes [BUG] Category Filter not working in v0.12.0 Fixes #990 * feat(profile): add record holders for activities and display details in profile page * feat: restructure issue templates and enhance contribution guidelines * Potential fix for code scanning alert no. 50: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for code scanning alert no. 51: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --------- Co-authored-by: 橙 <chengjunchao@hotmail.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
275 lines
13 KiB
Python
275 lines
13 KiB
Python
from rest_framework import viewsets
|
|
from rest_framework.response import Response
|
|
from rest_framework.decorators import action
|
|
from django.shortcuts import get_object_or_404
|
|
from adventures.utils.sports_types import SPORT_CATEGORIES
|
|
from adventures.utils.get_is_visited import is_location_visited
|
|
from django.db.models import Sum, Avg, Max, Count
|
|
from worldtravel.models import City, Region, Country, VisitedCity, VisitedRegion
|
|
from adventures.models import Location, Collection, Activity
|
|
from django.contrib.auth import get_user_model
|
|
|
|
User = get_user_model()
|
|
|
|
class StatsViewSet(viewsets.ViewSet):
|
|
"""
|
|
A simple ViewSet for listing the stats of a user.
|
|
"""
|
|
|
|
def _get_visited_locations_count(self, user):
|
|
"""Calculate count of visited locations for a user"""
|
|
visited_count = 0
|
|
|
|
# Get all locations for this user
|
|
user_locations = Location.objects.filter(user=user).prefetch_related('visits')
|
|
|
|
for location in user_locations:
|
|
if is_location_visited(location):
|
|
visited_count += 1
|
|
|
|
return visited_count
|
|
|
|
def _can_view_location(self, request_user, profile_user, location):
|
|
if not location:
|
|
return False
|
|
|
|
if request_user.is_authenticated and request_user.id == profile_user.id:
|
|
return True
|
|
|
|
return bool(location.is_public)
|
|
|
|
def _build_activity_record(self, activity, metric_key, metric_value, profile_user, request_user):
|
|
if not activity:
|
|
return None
|
|
|
|
location = activity.visit.location if activity.visit_id and activity.visit else None
|
|
can_view_location = self._can_view_location(request_user, profile_user, location)
|
|
|
|
return {
|
|
'metric_key': metric_key,
|
|
'metric_value': round(float(metric_value or 0), 2),
|
|
'activity_id': str(activity.id),
|
|
'activity_name': activity.name if activity.name else None,
|
|
'sport_type': activity.sport_type,
|
|
'start_date': activity.start_date.isoformat() if activity.start_date else None,
|
|
'location_id': str(location.id) if (location and can_view_location) else None,
|
|
'location_name': location.name if (location and can_view_location) else None,
|
|
}
|
|
|
|
def _get_activity_stats_by_category(self, user_activities, profile_user, request_user):
|
|
"""Calculate detailed stats for each sport category"""
|
|
category_stats = {}
|
|
|
|
for category, sports in SPORT_CATEGORIES.items():
|
|
activities = user_activities.filter(sport_type__in=sports)
|
|
|
|
if activities.exists():
|
|
# Calculate aggregated stats
|
|
stats = activities.aggregate(
|
|
count=Count('id'),
|
|
total_distance=Sum('distance'),
|
|
total_moving_time=Sum('moving_time'),
|
|
total_elevation_gain=Sum('elevation_gain'),
|
|
total_elevation_loss=Sum('elevation_loss'),
|
|
avg_distance=Avg('distance'),
|
|
max_distance=Max('distance'),
|
|
avg_elevation_gain=Avg('elevation_gain'),
|
|
max_elevation_gain=Max('elevation_gain'),
|
|
avg_speed=Avg('average_speed'),
|
|
max_speed=Max('max_speed'),
|
|
total_calories=Sum('calories')
|
|
)
|
|
|
|
# Convert Duration objects to total seconds for JSON serialization
|
|
total_moving_seconds = 0
|
|
if stats['total_moving_time']:
|
|
total_moving_seconds = int(stats['total_moving_time'].total_seconds())
|
|
|
|
# Get sport type breakdown within category
|
|
sport_breakdown = {}
|
|
for sport in sports:
|
|
sport_activities = activities.filter(sport_type=sport)
|
|
if sport_activities.exists():
|
|
sport_stats = sport_activities.aggregate(
|
|
count=Count('id'),
|
|
total_distance=Sum('distance'),
|
|
total_elevation_gain=Sum('elevation_gain')
|
|
)
|
|
sport_breakdown[sport] = {
|
|
'count': sport_stats['count'],
|
|
'total_distance': round(sport_stats['total_distance'] or 0, 2),
|
|
'total_elevation_gain': round(sport_stats['total_elevation_gain'] or 0, 2)
|
|
}
|
|
|
|
category_stats[category] = {
|
|
'count': stats['count'],
|
|
'total_distance': round(stats['total_distance'] or 0, 2),
|
|
'total_moving_time': total_moving_seconds,
|
|
'total_elevation_gain': round(stats['total_elevation_gain'] or 0, 2),
|
|
'total_elevation_loss': round(stats['total_elevation_loss'] or 0, 2),
|
|
'avg_distance': round(stats['avg_distance'] or 0, 2),
|
|
'max_distance': round(stats['max_distance'] or 0, 2),
|
|
'avg_elevation_gain': round(stats['avg_elevation_gain'] or 0, 2),
|
|
'max_elevation_gain': round(stats['max_elevation_gain'] or 0, 2),
|
|
'avg_speed': round(stats['avg_speed'] or 0, 2),
|
|
'max_speed': round(stats['max_speed'] or 0, 2),
|
|
'total_calories': round(stats['total_calories'] or 0, 2),
|
|
'sports': sport_breakdown,
|
|
'record_holders': {
|
|
'max_distance': self._build_activity_record(
|
|
activities.exclude(distance__isnull=True).select_related('visit__location').order_by('-distance', '-start_date').first(),
|
|
'distance',
|
|
stats['max_distance'],
|
|
profile_user,
|
|
request_user,
|
|
),
|
|
'max_speed': self._build_activity_record(
|
|
activities.exclude(max_speed__isnull=True).select_related('visit__location').order_by('-max_speed', '-start_date').first(),
|
|
'max_speed',
|
|
stats['max_speed'],
|
|
profile_user,
|
|
request_user,
|
|
),
|
|
'max_elevation_gain': self._build_activity_record(
|
|
activities.exclude(elevation_gain__isnull=True).select_related('visit__location').order_by('-elevation_gain', '-start_date').first(),
|
|
'elevation_gain',
|
|
stats['max_elevation_gain'],
|
|
profile_user,
|
|
request_user,
|
|
),
|
|
'max_calories': self._build_activity_record(
|
|
activities.exclude(calories__isnull=True).select_related('visit__location').order_by('-calories', '-start_date').first(),
|
|
'calories',
|
|
activities.exclude(calories__isnull=True).aggregate(value=Max('calories'))['value'],
|
|
profile_user,
|
|
request_user,
|
|
),
|
|
}
|
|
}
|
|
|
|
return category_stats
|
|
|
|
def _get_overall_activity_stats(self, user_activities, profile_user, request_user):
|
|
"""Calculate overall activity statistics"""
|
|
if not user_activities.exists():
|
|
return {
|
|
'total_count': 0,
|
|
'total_distance': 0,
|
|
'total_moving_time': 0,
|
|
'total_elevation_gain': 0,
|
|
'total_elevation_loss': 0,
|
|
'total_calories': 0,
|
|
'record_holders': {
|
|
'max_distance': None,
|
|
'max_speed': None,
|
|
'max_elevation_gain': None,
|
|
'max_calories': None,
|
|
},
|
|
}
|
|
|
|
stats = user_activities.aggregate(
|
|
total_count=Count('id'),
|
|
total_distance=Sum('distance'),
|
|
total_moving_time=Sum('moving_time'),
|
|
total_elevation_gain=Sum('elevation_gain'),
|
|
total_elevation_loss=Sum('elevation_loss'),
|
|
total_calories=Sum('calories')
|
|
)
|
|
|
|
# Convert Duration to seconds
|
|
total_moving_seconds = 0
|
|
if stats['total_moving_time']:
|
|
total_moving_seconds = int(stats['total_moving_time'].total_seconds())
|
|
|
|
return {
|
|
'total_count': stats['total_count'],
|
|
'total_distance': round(stats['total_distance'] or 0, 2),
|
|
'total_moving_time': total_moving_seconds,
|
|
'total_elevation_gain': round(stats['total_elevation_gain'] or 0, 2),
|
|
'total_elevation_loss': round(stats['total_elevation_loss'] or 0, 2),
|
|
'total_calories': round(stats['total_calories'] or 0, 2),
|
|
'record_holders': {
|
|
'max_distance': self._build_activity_record(
|
|
user_activities.exclude(distance__isnull=True).select_related('visit__location').order_by('-distance', '-start_date').first(),
|
|
'distance',
|
|
user_activities.exclude(distance__isnull=True).aggregate(value=Max('distance'))['value'],
|
|
profile_user,
|
|
request_user,
|
|
),
|
|
'max_speed': self._build_activity_record(
|
|
user_activities.exclude(max_speed__isnull=True).select_related('visit__location').order_by('-max_speed', '-start_date').first(),
|
|
'max_speed',
|
|
user_activities.exclude(max_speed__isnull=True).aggregate(value=Max('max_speed'))['value'],
|
|
profile_user,
|
|
request_user,
|
|
),
|
|
'max_elevation_gain': self._build_activity_record(
|
|
user_activities.exclude(elevation_gain__isnull=True).select_related('visit__location').order_by('-elevation_gain', '-start_date').first(),
|
|
'elevation_gain',
|
|
user_activities.exclude(elevation_gain__isnull=True).aggregate(value=Max('elevation_gain'))['value'],
|
|
profile_user,
|
|
request_user,
|
|
),
|
|
'max_calories': self._build_activity_record(
|
|
user_activities.exclude(calories__isnull=True).select_related('visit__location').order_by('-calories', '-start_date').first(),
|
|
'calories',
|
|
user_activities.exclude(calories__isnull=True).aggregate(value=Max('calories'))['value'],
|
|
profile_user,
|
|
request_user,
|
|
),
|
|
},
|
|
}
|
|
|
|
@action(detail=False, methods=['get'], url_path=r'counts/(?P<username>[\w.@+-]+)')
|
|
def counts(self, request, username):
|
|
if request.user.username == username:
|
|
user = get_object_or_404(User, username=username)
|
|
else:
|
|
user = get_object_or_404(User, username=username, public_profile=True)
|
|
|
|
# remove the email address from the response
|
|
user.email = None
|
|
|
|
# get the counts for the user
|
|
location_count = Location.objects.filter(user=user.id).count()
|
|
visited_location_count = self._get_visited_locations_count(user)
|
|
trips_count = Collection.objects.filter(user=user.id).count()
|
|
visited_city_count = VisitedCity.objects.filter(user=user.id).count()
|
|
total_cities = City.objects.count()
|
|
visited_region_count = VisitedRegion.objects.filter(user=user.id).count()
|
|
total_regions = Region.objects.count()
|
|
visited_country_count = VisitedRegion.objects.filter(
|
|
user=user.id).values('region__country').distinct().count()
|
|
total_countries = Country.objects.count()
|
|
|
|
# get activity data
|
|
user_activities = Activity.objects.filter(user=user.id)
|
|
|
|
# Get enhanced activity statistics
|
|
overall_activity_stats = self._get_overall_activity_stats(user_activities, user, request.user)
|
|
activity_stats_by_category = self._get_activity_stats_by_category(user_activities, user, request.user)
|
|
|
|
return Response({
|
|
# Travel stats
|
|
'location_count': location_count,
|
|
'visited_location_count': visited_location_count,
|
|
'trips_count': trips_count,
|
|
'visited_city_count': visited_city_count,
|
|
'total_cities': total_cities,
|
|
'visited_region_count': visited_region_count,
|
|
'total_regions': total_regions,
|
|
'visited_country_count': visited_country_count,
|
|
'total_countries': total_countries,
|
|
|
|
# Overall activity stats
|
|
'activities_overall': overall_activity_stats,
|
|
|
|
# Detailed activity stats by category
|
|
'activities_by_category': activity_stats_by_category,
|
|
|
|
# Legacy fields (for backward compatibility)
|
|
'activity_distance': overall_activity_stats['total_distance'],
|
|
'activity_moving_time': overall_activity_stats['total_moving_time'],
|
|
'activity_elevation': overall_activity_stats['total_elevation_gain'],
|
|
'activity_count': overall_activity_stats['total_count'],
|
|
}) |