Skip to content

Commit

Permalink
Merge pull request #207 from Diary-workout-tracker/feature/correct-ti…
Browse files Browse the repository at this point in the history
…mezone-endpoint

Feature/correct timezone endpoint
  • Loading branch information
VladislavYar authored May 30, 2024
2 parents 51edf5b + ca024e2 commit ada7fd8
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 98 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12"]
python-version: ["3.11"]

services:
postgres:
Expand Down
70 changes: 53 additions & 17 deletions backend/api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from users.models import User as ClassUser
from utils.authcode import AuthCode
from utils.users import get_user_by_email_or_404
from utils.amount_skips import counts_missed_days

from .constants import FORMAT_DATE, FORMAT_DATETIME, FORMAT_TIME
from .fields import Base64ImageField
Expand All @@ -36,6 +37,13 @@ class UserTimezoneSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ("timezone",)
extra_kwargs = {"timezone": {"required": True}}

def validate_timezone(self, value: str) -> str:
"""Валидация timezone пользователя."""
if value in pytz.all_timezones_set:
return value
raise serializers.ValidationError("Несуществующая timezone.")


class MeSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -94,18 +102,17 @@ def validate_amount_of_skips(self, value: int) -> int:
return value

def validate_date_last_skips(self, value: datetime) -> datetime:
user = self.context["request"].user
user: ClassUser = self.context["request"].user
date_last_skips = user.date_last_skips
user_timezone = pytz.timezone(user.timezone)
localdate = timezone.localdate(timezone=user_timezone)
user_timezone_value = value.astimezone(user_timezone).date()
if not date_last_skips:
if localdate != user_timezone_value:
raise serializers.ValidationError("День заморозки должен быть равен текущему дню.")
return value
date_last_skips = date_last_skips.astimezone(user_timezone).date()
if date_last_skips >= user_timezone_value:
raise serializers.ValidationError(f"День заморозки должен быть больше {date_last_skips}.")
if localdate != user_timezone_value:
raise serializers.ValidationError("День заморозки должен быть равен текущему дню.")
if date_last_skips:
date_last_skips = date_last_skips.astimezone(user_timezone).date()
if date_last_skips == user_timezone_value:
raise serializers.ValidationError("Тренировка уже заморожена.")
return value

def create(self, validated_data: dict) -> ClassUser:
Expand Down Expand Up @@ -142,7 +149,7 @@ def validate(self, attrs):
authcode = AuthCode(user)
if authcode.code_is_valid(attrs["code"]):
return attrs
raise serializers.ValidationError({"code": ["Неверный или устаревший код"]})
raise serializers.ValidationError({"code": ["Неверный или устаревший код."]})

def create(self, validated_data):
user = User.objects.get(email=validated_data["email"])
Expand Down Expand Up @@ -260,7 +267,7 @@ def to_representation(self, instance: History) -> dict:
def validate(self, data: OrderedDict) -> OrderedDict:
if data["training_start"] >= data["training_end"]:
raise serializers.ValidationError(
{"training_start_training_end": ["Время начала тренировки должно быть раньше конца"]}
{"training_start_training_end": ["Время начала тренировки должно быть раньше конца."]}
)
return data

Expand All @@ -276,30 +283,41 @@ def _validate_date(self, value: datetime, name_field: str) -> datetime:
raise serializers.ValidationError("Дата должна быть больше прошлой тренировки.")
return value

def _check_lock_training(self, value: datetime) -> None:
"""Проверка блокировки челленджа."""
user: ClassUser = self.context["request"].user
if not user.last_completed_training:
return
now = value.astimezone(pytz.timezone(user.timezone))
days_missed, *_ = counts_missed_days(user, user.timezone, now)
if user.amount_of_skips < days_missed:
raise serializers.ValidationError("Невозможно сохранить тренировку при заблокированном челлендже.")

def validate_training_start(self, value: datetime) -> datetime:
self._check_lock_training(value)
return self._validate_date(value, "training_start")

def validate_training_end(self, value: datetime) -> datetime:
return self._validate_date(value, "training_end")

def validate_motivation_phrase(self, value: str) -> str:
if not MotivationalPhrase.objects.filter(text=value).exists():
raise serializers.ValidationError("Данной мотивационной фразы не существует")
raise serializers.ValidationError("Данной мотивационной фразы не существует.")
return value

def validate_training_day(self, value: Day) -> Day:
last_completed_training = self.context["request"].user.last_completed_training
if last_completed_training:
day_number_last_training = last_completed_training.training_day.day_number
if value.day_number - 1 != day_number_last_training:
raise serializers.ValidationError(f"День тренировки должен быть равен {day_number_last_training+1}")
raise serializers.ValidationError(f"День тренировки должен быть равен {day_number_last_training+1}.")
elif value.day_number != 1:
raise serializers.ValidationError("День тренировки должен быть равен 1")
raise serializers.ValidationError("День тренировки должен быть равен 1.")
return value

def validate_achievements(self, value: list) -> list:
if value and len(value) > Achievement.objects.filter(id__in=value).count():
raise serializers.ValidationError("Некорректные ачивки")
raise serializers.ValidationError("Некорректные ачивки.")
return value

def get_time(self, obj: History) -> int:
Expand All @@ -314,7 +332,25 @@ def create(self, validated_data: dict) -> History:
return super().create(validated_data)


class BoolSerializer(serializers.Serializer):
"""Сериализатор bool значения."""
class ResponseUserDefaultSerializer(serializers.Serializer):
"""Сериализатор возвращаемого значения UserDefaultView."""

default = serializers.BooleanField()


class ResponseUpdateSerializer(serializers.Serializer):
"""Сериализатор возрващаемого значения UpdateView."""

enough = serializers.BooleanField()


class ResponseResendCodeSerializer(serializers.Serializer):
"""Сериализатор возрващаемого значения ResendCodeView."""

result = serializers.CharField(default="Код создан и отправлен")


class ResponseHealthCheckSerializer(serializers.Serializer):
"""Сериализатор возрващаемого значения HealthCheckView."""

updated = serializers.BooleanField()
Health = serializers.CharField(default="OK")
123 changes: 70 additions & 53 deletions backend/api/v1/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime, timedelta
from datetime import datetime

import pytz
from django.contrib.auth import get_user_model
Expand All @@ -11,20 +11,24 @@
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView

from rest_framework_simplejwt.serializers import TokenRefreshSerializer
from running.models import Achievement, Day, History, UserAchievement
from users.constants import DEFAULT_AMOUNT_OF_SKIPS
from users.models import User as ClassUser
from utils import authcode, mailsender, motivation_phrase, users
from utils.achievements import AchievementUpdater
from utils.amount_skips import counts_missed_days

from .serializers import (
AchievementEndTrainingSerializer,
AchievementSerializer,
BoolSerializer,
CustomTokenObtainSerializer,
HistorySerializer,
MeSerializer,
ResponseHealthCheckSerializer,
ResponseResendCodeSerializer,
ResponseUpdateSerializer,
ResponseUserDefaultSerializer,
TrainingSerializer,
UserSerializer,
UserTimezoneSerializer,
Expand All @@ -44,14 +48,23 @@ class HealthCheckView(APIView):
permission_classes = (AllowAny,)

@extend_schema(
responses={200: ResponseHealthCheckSerializer()},
summary="Проверка работы",
description="Проверка работы АПИ",
description="Проверка работы API",
tags=("System",),
)
def get(self, request):
return Response({"Health": "OK"})


@extend_schema_view(
post=extend_schema(
responses={201: UserSerializer()},
summary="Создание пользователя",
description="Создание пользователя",
tags=("User",),
),
)
class RegisterUserView(APIView):
serializer_class = UserSerializer
permission_classes = (AllowAny,)
Expand All @@ -67,9 +80,10 @@ def post(self, request, format=None):

@extend_schema_view(
post=extend_schema(
responses={201: ResponseResendCodeSerializer()},
summary="Повторная отправка кода",
description="Повторная отправка кода",
tags=("api",),
tags=("User",),
),
)
class ResendCodeView(APIView):
Expand All @@ -83,6 +97,14 @@ def post(self, request):
return Response({"result": "Код создан и отправлен"}, status=status.HTTP_201_CREATED)


@extend_schema_view(
post=extend_schema(
responses={200: TokenRefreshSerializer()},
summary="Обновление токена",
description="Обновление токена",
tags=("User",),
),
)
class TokenRefreshView(APIView):
serializer_class = CustomTokenObtainSerializer
throttle_classes = (DurationCooldownRequestThrottle,)
Expand All @@ -91,31 +113,39 @@ class TokenRefreshView(APIView):
def post(self, request, format=None):
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
token_data = serializer.save()
token_data: dict = serializer.save()
token_data.pop("refresh")
return Response(token_data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


class MyInfoView(APIView):
@extend_schema_view(
get=extend_schema(
responses={200: MeSerializer()},
summary="Отдаёт данные по пользователю",
description="Отдаёт данные по пользователю",
tags=("User",),
),
patch=extend_schema(
responses={200: MeSerializer()},
summary="Обновляет данные пользователя",
description="Обновляет данные пользователя",
tags=("User",),
),
put=extend_schema(exclude=True),
delete=extend_schema(
responses={204: MeSerializer()},
summary="Удаляет пользователя",
description="Удаляет пользователя",
tags=("User",),
),
)
class MyInfoView(generics.RetrieveUpdateDestroyAPIView):
serializer_class = MeSerializer

def get(self, request, *args, **kwargs):
user = request.user
serializer = self.serializer_class(user, context={"request": request})
return Response(serializer.data)

def patch(self, request, *args, **kwargs):
user = request.user
serializer = self.serializer_class(user, data=request.data, context={"request": request}, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

def delete(self, request, *args, **kwargs):
user = request.user
user.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
def get_object(self):
"""Отдаёт объект пользователя."""
return self.request.user


@extend_schema_view(
Expand Down Expand Up @@ -230,22 +260,15 @@ def create(self, request: Request, *args, **kwargs) -> Response:

@extend_schema_view(
patch=extend_schema(
responses={200: BoolSerializer()},
responses={200: ResponseUpdateSerializer()},
summary="Обновляет заморозки у пользователя и сохраняет часовой пояс",
description="Обновляет заморозки у пользователя и сохраняет часовой пояс",
tags=("System",),
tags=("User",),
),
)
class UpdateView(APIView):
serializer_class = UserTimezoneSerializer

def _get_date_activity(self, user: ClassUser, user_timezone: str) -> datetime:
"""Отдаёт дату последней активности в виде тренировки или заморозки."""
date_activity = max(
[date for date in [user.date_last_skips, user.last_completed_training.training_start] if date is not None]
)
return date_activity.astimezone(pytz.timezone(user_timezone))

def _updates_skip_data(
self, user: ClassUser, amount_of_skips: int, days_missed: int, date_day_ago: datetime
) -> None:
Expand All @@ -254,12 +277,10 @@ def _updates_skip_data(
user.date_last_skips = date_day_ago
user.save()

def _clearing_user_training_data(self, user: ClassUser) -> None:
"""Очищает данные по тренировка пользователя."""
user.amount_of_skips = DEFAULT_AMOUNT_OF_SKIPS
user.date_last_skips = None
def _set_null_amount_of_skip(self, user: ClassUser) -> None:
"""Устанавливает значение заморозок у пользователя равное нулю."""
user.amount_of_skips = 0
user.save()
History.objects.filter(user_id=user).delete()

def _update_user_timezone_data(self, user: ClassUser, user_timezone: str) -> None:
"""Обновляет timezone ползователя."""
Expand All @@ -271,36 +292,32 @@ def patch(self, request: Request, *args, **kwargs) -> Response:
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
user_timezone = request.data.get("timezone")
response = Response({"updated": True}, status=status.HTTP_200_OK)
user = request.user
last_traning = user.last_completed_training
response = Response({"enough": True}, status=status.HTTP_200_OK)
user: ClassUser = request.user
last_traning: History = user.last_completed_training

if not last_traning or last_traning.training_day.day_number == 100:
self._update_user_timezone_data(user, user_timezone)
return response

date_activity = self._get_date_activity(user, user_timezone)
amount_of_skips = user.amount_of_skips
date_day_ago = timezone.localtime(timezone=pytz.timezone(user_timezone)) - timedelta(days=1)
days_missed = (date_day_ago.date() - date_activity.date()).days
now = timezone.localtime(timezone=pytz.timezone(user_timezone))
days_missed, date_day_ago, amount_of_skips = counts_missed_days(user, user_timezone, now)
if days_missed <= 0:
self._update_user_timezone_data(user, user_timezone)
return response

user.timezone = user_timezone
if amount_of_skips >= days_missed:
self._updates_skip_data(user, amount_of_skips, days_missed, date_day_ago)
else:
self._clearing_user_training_data(user)
return response
return response
self._set_null_amount_of_skip(user)
return Response({"enough": False}, status=status.HTTP_200_OK)


@extend_schema_view(
patch=extend_schema(
responses={200: BoolSerializer()},
responses={200: ResponseUserDefaultSerializer()},
summary="Очищает данные по тренировкам и ачивки пользователя",
description="Очищает данные по тренировкам и ачивки пользователя",
tags=("System",),
tags=("User",),
),
)
class UserDefaultView(APIView):
Expand All @@ -315,4 +332,4 @@ def patch(self, request: Request, *args, **kwargs) -> Response:
user_history.delete()
user_achievements: QuerySet[UserAchievement] = user.user_achievements.all()
user_achievements.delete()
return Response({"updated": True}, status=status.HTTP_200_OK)
return Response({"default": True}, status=status.HTTP_200_OK)
Loading

0 comments on commit ada7fd8

Please sign in to comment.