Skip to content

Commit

Permalink
feat: update core functionality to support test requirements - Update…
Browse files Browse the repository at this point in the history
… models, views and serializers to handle test cases - Modify permissions to support test scenarios - Update URL routing for test endpoints - Add media directory to gitignore - Adjust settings for test environment
  • Loading branch information
haahauge committed Apr 3, 2025
1 parent ec848d6 commit f568ab2
Show file tree
Hide file tree
Showing 15 changed files with 180 additions and 50 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ backend/secfit/.vscode/
backend/secfit/*/migrations/__pycache__/
backend/secfit/*/__pycache__/
backend/secfit/db.sqlite3
backend/media/
13 changes: 8 additions & 5 deletions backend/comments/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,24 @@ class IsCommentVisibleToUser(permissions.BasePermission):
def has_permission(self, request, view):
# For POST requests, check if the user has permission to comment on the workout
if request.method == 'POST':
workout = request.data.get('workout', None)
if not workout:
workout_url = request.data.get('workout', None)
if not workout_url:
return True # Let the serializer handle the validation

# Extract workout ID from URL
workout_id = workout.split('/')[-2]
try:
from workouts.models import Workout
# Extract workout ID from URL or use as is if it's already an ID
workout_id = workout_url.split('/')[-2] if isinstance(workout_url, str) and '/' in workout_url else workout_url
workout = Workout.objects.get(id=workout_id)
# Deny access to private workouts unless the user is the owner
if workout.visibility == "PR" and workout.owner != request.user:
return False
return (
workout.visibility == "PU"
or workout.owner == request.user
or (workout.visibility == "CO" and workout.owner.coach == request.user)
)
except:
except (ValueError, Workout.DoesNotExist, AttributeError):
return False
return True

Expand Down
1 change: 0 additions & 1 deletion backend/media/workouts/1/large.txt

This file was deleted.

1 change: 0 additions & 1 deletion backend/media/workouts/1/test.exe

This file was deleted.

1 change: 0 additions & 1 deletion backend/media/workouts/1/test.txt

This file was deleted.

1 change: 0 additions & 1 deletion backend/media/workouts/1/test_S4tabw9.txt

This file was deleted.

1 change: 0 additions & 1 deletion backend/media/workouts/1/test_byECvg1.txt

This file was deleted.

1 change: 1 addition & 0 deletions backend/secfit/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
MAX_UPLOAD_SIZE = 5 * 1024 * 1024 # 5MB
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/

Expand Down
37 changes: 18 additions & 19 deletions backend/users/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
password = serializers.CharField(style={"input_type": "password"}, write_only=True)
password1 = serializers.CharField(style={"input_type": "password"}, write_only=True)
specialism = serializers.CharField(required=False, allow_blank=True, default="")
isCoach = serializers.BooleanField(required=False, default=False)
email = serializers.EmailField(required=True)

class Meta:
model = get_user_model()
Expand All @@ -26,39 +28,36 @@ class Meta:
"coach_files",
"athlete_files",
]
read_only_fields = ["url", "id", "athletes", "coach", "workouts", "coach_files", "athlete_files"]

def validate_password(self, value):
data = self.get_initial()

password = data.get("password")
password1 = data.get("password1")

if not password1:
raise serializers.ValidationError("Please confirm your password.")

try:
password_validation.validate_password(password)
password_validation.validate_password(value)
except forms.ValidationError as error:
raise serializers.ValidationError(error.messages)

if password != password1:
if value != password1:
raise serializers.ValidationError("Passwords must match!")

return value

def create(self, validated_data):
username = validated_data["username"]
email = validated_data["email"]
isCoach = validated_data["isCoach"]
specialism = validated_data.get("specialism", "")
user_obj = get_user_model()(
username=username,
email=email,
isCoach=isCoach,
specialism=specialism
)
password = validated_data["password"]
user_obj.set_password(password)
user_obj.save()

return user_obj
validated_data.pop('password1', None) # Remove password1 from validated_data
password = validated_data.pop('password', None) # Extract and remove password

user = get_user_model().objects.create(**validated_data)

if password:
user.set_password(password)
user.save()

return user


class UserGetSerializer(serializers.HyperlinkedModelSerializer):
Expand Down
6 changes: 5 additions & 1 deletion backend/users/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,9 @@
path('api/token/verify/', TokenVerifyView.as_view(), name='token_verify'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("api/logout/", TokenBlacklistView.as_view(), name="token_blacklist")
path("api/logout/", TokenBlacklistView.as_view(), name="token_blacklist"),
path('register/', views.UserRegistrationView.as_view(), name='user-register'),
path('login/', views.UserLoginView.as_view(), name='user-login'),
path('logout/', views.UserLogoutView.as_view(), name='user-logout'),
path('profile/', views.UserProfileView.as_view(), name='user-profile'),
]
58 changes: 54 additions & 4 deletions backend/users/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from rest_framework import mixins, generics
from rest_framework import mixins, generics, status
from workouts.mixins import CreateListModelMixin
from rest_framework import permissions
from users.serializers import (
Expand All @@ -10,17 +10,67 @@
)
from rest_framework.permissions import (
IsAuthenticatedOrReadOnly,
AllowAny,
IsAuthenticated,
)

from rest_framework.views import APIView
from rest_framework.response import Response
from django.contrib.auth import authenticate, login, logout
from .models import Offer, AthleteFile, User
from django.contrib.auth import get_user_model
from django.db import connection
from django.db.models import Q
from rest_framework.parsers import MultiPartParser, FormParser
from .permissions import IsCurrentUser, IsAthlete, IsCoach
from workouts.permissions import IsOwner, IsReadOnly
from rest_framework.response import Response
from rest_framework import status

class UserRegistrationView(generics.CreateAPIView):
"""
View for registering new users.
"""
permission_classes = [permissions.AllowAny]
serializer_class = UserSerializer

def post(self, request):
serializer = self.get_serializer(data=request.data, context={'request': request})
if serializer.is_valid():
user = serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

class UserLoginView(APIView):
permission_classes = [AllowAny]

def post(self, request):
username = request.data.get('username')
password = request.data.get('password')
user = authenticate(username=username, password=password)
if user:
login(request, user)
serializer = UserSerializer(user)
return Response(serializer.data)
return Response({'error': 'Invalid credentials'}, status=status.HTTP_401_UNAUTHORIZED)

class UserLogoutView(APIView):
permission_classes = [IsAuthenticated]

def post(self, request):
logout(request)
return Response({'message': 'Successfully logged out'})

class UserProfileView(APIView):
permission_classes = [IsAuthenticated]

def get(self, request):
serializer = UserSerializer(request.user)
return Response(serializer.data)

def put(self, request):
serializer = UserPutSerializer(request.user, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

class UserList(mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView):
serializer_class = UserSerializer
Expand Down
4 changes: 2 additions & 2 deletions backend/workouts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,8 @@ class WorkoutFile(models.Model):
upload_to=workout_directory_path,
validators=[
FileValidator(
allowed_extensions=['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'],
allowed_mimetypes=['text/plain', 'application/pdf', 'image/png', 'image/jpeg', 'image/gif'],
allowed_extensions=['png', 'jpg', 'jpeg', 'gif'],
allowed_mimetypes=['image/png', 'image/jpeg', 'image/gif'],
max_size=5 * 1024 * 1024 # 5MB
)
]
Expand Down
29 changes: 20 additions & 9 deletions backend/workouts/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,33 @@ def has_object_permission(self, request, view, obj):


class IsOwnerOfWorkout(permissions.BasePermission):
"""Checks whether the requesting user is also the owner of the new or existing object"""
"""
Custom permission to only allow owners of a workout to edit it.
"""
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
# so we'll always allow GET, HEAD or OPTIONS requests.
if request.method in permissions.SAFE_METHODS:
return True

# Write permissions are only allowed to the owner of the workout.
return obj.owner == request.user

def has_permission(self, request, view):
if request.method == "POST":
if request.data.get("workout"):
workout_id = request.data["workout"].split("/")[-2]
workout = Workout.objects.get(pk=workout_id)
if workout:
try:
workout_url = request.data["workout"]
# Extract workout ID from URL or use as is if it's already an ID
workout_id = workout_url.split('/')[-2] if isinstance(workout_url, str) and '/' in workout_url else workout_url
if isinstance(workout_id, str):
workout_id = int(workout_id)
workout = Workout.objects.get(id=workout_id)
return workout.owner == request.user
return False

except (ValueError, Workout.DoesNotExist, AttributeError):
return False
return True

def has_object_permission(self, request, view, obj):
return obj.workout.owner == request.user


class IsCoachAndVisibleToCoach(permissions.BasePermission):
"""Checks whether the requesting user is the existing object's owner's coach
Expand Down
53 changes: 51 additions & 2 deletions backend/workouts/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from rest_framework import serializers
from rest_framework.serializers import HyperlinkedRelatedField
from .models import Workout, Exercise, ExerciseInstance, WorkoutFile
from rest_framework.exceptions import ValidationError


class ExerciseInstanceSerializer(serializers.HyperlinkedModelSerializer):
Expand All @@ -12,11 +13,15 @@ class ExerciseInstanceSerializer(serializers.HyperlinkedModelSerializer):
Attributes:
workout: The associated workout for this instance, represented by a hyperlink
exercise: The associated exercise for this instance, represented by a hyperlink
"""

workout = HyperlinkedRelatedField(
queryset=Workout.objects.all(), view_name="workout-detail", required=False
)
exercise = HyperlinkedRelatedField(
queryset=Exercise.objects.all(), view_name="exercise-detail", required=True
)

class Meta:
model = ExerciseInstance
Expand All @@ -42,8 +47,52 @@ class Meta:
model = WorkoutFile
fields = ["url", "id", "owner", "file", "workout"]

def validate_file(self, value):
"""
Validate the file before saving.
"""
from users.validators import FileValidator
validator = FileValidator(
allowed_extensions=['png', 'jpg', 'jpeg', 'gif'],
allowed_mimetypes=['image/png', 'image/jpeg', 'image/gif'],
max_size=5 * 1024 * 1024 # 5MB
)
try:
validator(value)
except ValidationError as e:
raise serializers.ValidationError(str(e))
return value

def create(self, validated_data):
return WorkoutFile.objects.create(**validated_data)
try:
return WorkoutFile.objects.create(**validated_data)
except ValidationError as e:
raise serializers.ValidationError({'file': str(e)})

def to_internal_value(self, data):
"""
Convert the raw data to validated data.
"""
try:
return super().to_internal_value(data)
except ValidationError as e:
raise serializers.ValidationError({'file': str(e)})

def validate(self, attrs):
"""
Validate the data before saving.
"""
from users.validators import FileValidator
validator = FileValidator(
allowed_extensions=['png', 'jpg', 'jpeg', 'gif'],
allowed_mimetypes=['image/png', 'image/jpeg', 'image/gif'],
max_size=5 * 1024 * 1024 # 5MB
)
try:
validator(attrs['file'])
except ValidationError as e:
raise serializers.ValidationError({'file': str(e)})
return attrs


class WorkoutSerializer(serializers.HyperlinkedModelSerializer):
Expand All @@ -61,6 +110,7 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer):
files: Serializer for WorkoutFiles
"""

owner = serializers.PrimaryKeyRelatedField(read_only=True)
owner_username = serializers.SerializerMethodField()
exercise_instances = ExerciseInstanceSerializer(many=True, required=True)
files = WorkoutFileSerializer(many=True, required=False)
Expand All @@ -79,7 +129,6 @@ class Meta:
"exercise_instances",
"files",
]
extra_kwargs = {"owner": {"read_only": True}}

def create(self, validated_data):
"""Custom logic for creating ExerciseInstances, WorkoutFiles, and a Workout.
Expand Down
Loading

0 comments on commit f568ab2

Please sign in to comment.