Skip to content

Commit

Permalink
Merge branch 'develop' into feature/recurring_achievements
Browse files Browse the repository at this point in the history
  • Loading branch information
holohup authored Apr 4, 2024
2 parents 24a13d3 + 9188969 commit 548e0d8
Show file tree
Hide file tree
Showing 21 changed files with 306 additions and 174 deletions.
1 change: 0 additions & 1 deletion .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,3 @@ jobs:
sudo docker compose -f docker-compose.production.yml exec backend python manage.py collectstatic
sudo docker compose -f docker-compose.production.yml exec backend python manage.py migrate
sudo docker compose -f docker-compose.production.yml exec backend python manage.py loaddata fixture/achievements_fixture.json
sudo docker compose -f docker-compose.production.yml exec backend cp -r /app/static/. /backend_static/static/
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,5 @@ history_images/

#logs
logs/django*
*minio

5 changes: 1 addition & 4 deletions backend/api/v1/fields.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import base64
import datetime
import re

from django.conf import settings
from django.core.files.base import ContentFile
Expand All @@ -27,6 +26,4 @@ def to_representation(self, value):
uri = self.context["request"].build_absolute_uri(
f"{settings.MEDIA_URL}{value}" if isinstance(value, str) else value.url
)
if set(settings.CSRF_TRUSTED_ORIGINS).issubset(("http://127.0.0.1", "http://localhost")):
return uri
return re.sub("http", "https", uri, 1)
return uri
81 changes: 74 additions & 7 deletions backend/api/v1/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from collections import OrderedDict
from datetime import datetime
import pytz

from django.contrib.auth import get_user_model
from django.utils import timezone
from rest_framework import serializers
from rest_framework_simplejwt.tokens import RefreshToken

Expand All @@ -22,6 +24,16 @@ class UserSerializer(serializers.ModelSerializer):
"""Сериализатор кастомного пользователя."""

email = serializers.EmailField(validators=(CustomUniqueValidator(queryset=User.objects.all()),))

class Meta:
model = User
fields = ("email",)


class MeSerializer(serializers.ModelSerializer):
"""Сериализатор Me пользователя."""

email = serializers.EmailField(read_only=True)
name = serializers.CharField(required=False)
gender = serializers.ChoiceField(choices=GENDER_CHOICES, required=False)
height_cm = serializers.IntegerField(allow_null=True, required=False)
Expand All @@ -32,7 +44,6 @@ class UserSerializer(serializers.ModelSerializer):
date_last_skips = serializers.DateTimeField(required=False)
amount_of_skips = serializers.IntegerField(required=False)
avatar = Base64ImageField(allow_null=True, required=False)
timezone = serializers.CharField(required=False)

class Meta:
model = User
Expand All @@ -46,9 +57,49 @@ class Meta:
"date_last_skips",
"amount_of_skips",
"avatar",
"timezone",
)

def to_representation(self, instance: ClassUser) -> dict:
representation = super().to_representation(instance)
date_last_skips = instance.date_last_skips
if date_last_skips:
user_timezone = pytz.timezone(self.context["request"].user.timezone)
representation["date_last_skips"] = date_last_skips.astimezone(user_timezone).strftime(FORMAT_DATE)
return representation

def validate(self, data: OrderedDict) -> OrderedDict:
date_last_skips = data.get("date_last_skips")
amount_of_skips = data.get("amount_of_skips")
if [date_last_skips, amount_of_skips].count(None) == 1:
raise serializers.ValidationError(
{"date_last_skips_amount_of_skips": ["Поля должны присутствовать одновременно."]}
)
return data

def validate_amount_of_skips(self, value: int) -> int:
amount_of_skips = self.context["request"].user.amount_of_skips
if not amount_of_skips:
raise serializers.ValidationError("У пользователя отсутсвуют заморозки.")
new_amount_of_skips = amount_of_skips - 1
if new_amount_of_skips != value:
raise serializers.ValidationError(f"Заморозки должны быть равны {new_amount_of_skips}.")
return value

def validate_date_last_skips(self, value: datetime) -> datetime:
user = 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}.")
return value

def create(self, validated_data: dict) -> ClassUser:
"""Создаёт нового пользователя."""
avatar_data = validated_data.pop("avatar", None)
Expand Down Expand Up @@ -131,6 +182,14 @@ class Meta:
"received",
)

def to_representation(self, instance: Achievement) -> dict:
representation = super().to_representation(instance)
achievement_date = instance.achievement_date
if achievement_date:
user_timezone = pytz.timezone(self.context["request"].user.timezone)
representation["achievement_date"] = achievement_date.astimezone(user_timezone).strftime(FORMAT_DATE)
return representation


class AchievementEndTrainingSerializer(serializers.ModelSerializer):
"""Сериализатор достижения конца тренировки."""
Expand Down Expand Up @@ -179,12 +238,14 @@ class Meta:
"route": {"required": False},
}

def to_representation(self, instance):
def to_representation(self, instance: History) -> dict:
representation = super().to_representation(instance)
training_start = instance.training_start
user = self.context["request"].user
user_timezone = pytz.timezone(user.timezone)
representation["training_start"] = [
training_start.strftime(FORMAT_DATE),
training_start.strftime(FORMAT_DATETIME),
training_start.astimezone(user_timezone).strftime(FORMAT_DATE),
training_start.astimezone(user_timezone).strftime(FORMAT_DATETIME),
]
return representation

Expand All @@ -196,8 +257,14 @@ def validate(self, data: OrderedDict) -> OrderedDict:
return data

def _validate_date(self, value: datetime, name_field: str) -> datetime:
last_completed_training = self.context["request"].user.last_completed_training
if last_completed_training and value.date() <= getattr(last_completed_training, name_field).date():
user = self.context["request"].user
last_completed_training = user.last_completed_training
user_timezone = pytz.timezone(user.timezone)
if (
last_completed_training
and value.astimezone(user_timezone).date()
<= getattr(last_completed_training, name_field).astimezone(user_timezone).date()
):
raise serializers.ValidationError("Дата должна быть больше прошлой тренировки.")
return value

Expand Down
24 changes: 14 additions & 10 deletions backend/api/v1/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from datetime import datetime, timedelta
import pytz

from django.contrib.auth import get_user_model
from django.db.models import Case, DateTimeField, Exists, F, OuterRef, When
Expand All @@ -23,6 +24,7 @@
CustomTokenObtainSerializer,
HistorySerializer,
TrainingSerializer,
MeSerializer,
UserSerializer,
)
from .throttling import DurationCooldownRequestThrottle
Expand Down Expand Up @@ -86,7 +88,7 @@ def post(self, request, format=None):


class MyInfoView(APIView):
serializer_class = UserSerializer
serializer_class = MeSerializer

def get(self, request, *args, **kwargs):
user = request.user
Expand Down Expand Up @@ -197,6 +199,8 @@ def create(self, request: Request, *args, **kwargs) -> Response:
achievements = request.data.pop("achievements", None) # noqa
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
if "achievements" in serializer._validated_data.keys():
serializer._validated_data.pop("achievements")
history = self.perform_create(serializer)
self.request.user.last_completed_training = history
self.request.user.total_m_run += history.distance
Expand All @@ -220,11 +224,12 @@ def create(self, request: Request, *args, **kwargs) -> Response:
),
)
class UpdateView(APIView):
def _get_date_activity(self, user: ClassUser) -> datetime:
def _get_date_activity(self, user: ClassUser, user_timezone: str) -> datetime:
"""Отдаёт дату последней активности в виде тренировки или заморозки."""
return max(
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
Expand All @@ -242,11 +247,10 @@ def _clearing_user_training_data(self, user: ClassUser) -> None:
History.objects.filter(user_id=user).delete()

def _update_user_timezone_data(self, user: ClassUser, user_timezone: str) -> None:
"""Обновзляет timezone ползователя."""
if user.timezone == user_timezone:
return
user.timezone = user_timezone
user.save()
"""Обновляет timezone ползователя."""
if user.timezone != user_timezone:
user.timezone = user_timezone
user.save()

def post(self, request: Request, *args, **kwargs) -> Response:
user_timezone = request.data.get("timezone")
Expand All @@ -259,9 +263,9 @@ def post(self, request: Request, *args, **kwargs) -> Response:
self._update_user_timezone_data(user, user_timezone)
return response

date_activity = self._get_date_activity(user)
date_activity = self._get_date_activity(user, user_timezone)
amount_of_skips = user.amount_of_skips
date_day_ago = timezone.localtime() - timedelta(days=1)
date_day_ago = timezone.localtime(timezone=pytz.timezone(user_timezone)) - timedelta(days=1)
days_missed = (date_day_ago.date() - date_activity.date()).days
if days_missed <= 0:
self._update_user_timezone_data(user, user_timezone)
Expand Down
52 changes: 30 additions & 22 deletions backend/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@

WSGI_APPLICATION = "config.wsgi.application"

TIME_ZONE = "UTC"

DATABASES = {
"default": {
"ENGINE": os.getenv("DB_ENGINE", default="django.db.backends.postgresql"),
Expand All @@ -80,7 +82,7 @@
"HOST": os.getenv("DB_HOST", default="localhost"),
"PORT": os.getenv("DB_PORT", default=5432),
"PG_USER": os.getenv("PG_USER", default="user"),
"TIME_ZONE": os.getenv("TIME_ZONE", default="Europe/Moscow"),
"TIME_ZONE": TIME_ZONE,
}
}

Expand All @@ -103,8 +105,6 @@

LANGUAGE_CODE = "ru-RU"

TIME_ZONE = os.getenv("TIME_ZONE", default="Europe/Moscow")

USE_I18N = True

USE_TZ = True
Expand All @@ -115,34 +115,41 @@
if IS_AWS_ACTIVE:
STORAGES = {
"default": {
"BACKEND": "storages.backends.s3.S3Storage",
"OPTIONS": {
"bucket_name": "running-app",
"location": "media",
},
"BACKEND": "config.storage.S3MediaStorage",
},
"staticfiles": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
"OPTIONS": {
"location": "/static",
"base_url": "/static/",
},
"BACKEND": "config.storage.StaticS3Boto3Storage",
},
}
AWS_STORAGE_BUCKET_NAME = os.getenv("AWS_STORAGE_BUCKET_NAME")
AWS_S3_ENDPOINT_URL = os.getenv("AWS_S3_ENDPOINT_URL")
AWS_S3_ACCESS_KEY_ID = os.getenv("AWS_S3_ACCESS_KEY_ID")
AWS_S3_SECRET_ACCESS_KEY = os.getenv("AWS_S3_SECRET_ACCESS_KEY")

STATICFILES_LOCATION = "static"
MEDIAFILES_LOCATION = "media"

MINIO_ACCESS_KEY = os.getenv("MINIO_ROOT_USER")
MINIO_SECRET_KEY = os.getenv("MINIO_ROOT_PASSWORD")
MINIO_BUCKET_NAME = os.getenv("MINIO_BUCKET_NAME")
MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT")
MINIO_ACCESS_URL = os.getenv("MINIO_ACCESS_URL")
MINIO_STORAGE_USE_HTTPS = os.getenv("MINIO_STORAGE_USE_HTTPS", False) == "True"
MINIO_S3_SECURE_URLS = os.getenv("MINIO_S3_SECURE_URLS", False) == "True"

AWS_ACCESS_KEY_ID = MINIO_ACCESS_KEY
AWS_SECRET_ACCESS_KEY = MINIO_SECRET_KEY
AWS_STORAGE_BUCKET_NAME = MINIO_BUCKET_NAME
AWS_S3_ENDPOINT_URL = MINIO_ENDPOINT
AWS_S3_FILE_OVERWRITE = os.getenv("AWS_S3_FILE_OVERWRITE", False) == "True"
AWS_S3_SIGNATURE_VERSION = os.getenv("AWS_S3_SIGNATURE_VERSION")
AWS_S3_USE_SSL = os.getenv("AWS_S3_USE_SSL", False) == "True"
AWS_S3_SECURE_URLS = os.getenv("AWS_S3_SECURE_URLS", False) == "True"
AWS_S3_URL_PROTOCOL = os.getenv("AWS_S3_URL_PROTOCOL", "http:")

MEDIA_URL = f"{AWS_S3_ENDPOINT_URL}/{AWS_STORAGE_BUCKET_NAME}/media/"
else:
MEDIA_URL = "/media/"
MEDIA_URL = "/media/"
STATIC_URL = "/static/"

MEDIA_ROOT = os.path.join(BASE_DIR, "media")

STATIC_URL = "/static/"
if DEBUG:

if DEBUG is True:
STATICFILES_DIRS = (os.path.join(BASE_DIR, "static/"),)
else:
STATIC_ROOT = os.path.join(BASE_DIR, "static")
Expand All @@ -152,6 +159,7 @@
FORM_RENDERER = "django.forms.renderers.TemplatesSetting"

REST_FRAMEWORK = {
"DATETIME_INPUT_FORMATS": ["%Y-%m-%d %H:%M:%S"],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
Expand Down
22 changes: 22 additions & 0 deletions backend/config/storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from django.conf import settings
from storages.backends.s3boto3 import S3Boto3Storage


class StaticS3Boto3Storage(S3Boto3Storage):
location = settings.STATICFILES_LOCATION

def __init__(self, *args, **kwargs):
if settings.MINIO_ACCESS_URL:
self.secure_urls = False
self.custom_domain = settings.MINIO_ACCESS_URL
super(StaticS3Boto3Storage, self).__init__(*args, **kwargs)


class S3MediaStorage(S3Boto3Storage):
location = settings.MEDIAFILES_LOCATION

def __init__(self, *args, **kwargs):
if settings.MINIO_ACCESS_URL:
self.secure_urls = False
self.custom_domain = settings.MINIO_ACCESS_URL
super(S3MediaStorage, self).__init__(*args, **kwargs)
24 changes: 0 additions & 24 deletions backend/static/core/css/workout_program.css

This file was deleted.

Loading

0 comments on commit 548e0d8

Please sign in to comment.