Skip to content

Commit

Permalink
Merge branch 'main' into feature/tasks_api
Browse files Browse the repository at this point in the history
  • Loading branch information
greenpandorik authored Jan 27, 2024
2 parents a6e6677 + 486ae52 commit 2e3154c
Show file tree
Hide file tree
Showing 18 changed files with 365 additions and 92 deletions.
1 change: 0 additions & 1 deletion .envexample
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ USE_POSTGRESQL=False
DB_NAME=IPR
DB_HOST=db
DB_PORT=5432
USE_POSTGRESQL=True
POSTGRES_DB=ipr
POSTGRES_USER=ipr_user
POSTGRES_PASSWORD=ipr_password
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Backend Проекта "ИПР". Хакатон Яндекс-Альфа-Банк. Команда №8

![GitHub Actions](https://github.com/Reagent992/ipr-hackathon-yandex-alfa/blob/main/.github/workflows/code-style.yml/badge.svg)\
![Code-Style](https://github.com/Reagent992/ipr-hackathon-yandex-alfa/actions/workflows/code-style.yml/badge.svg)\
![Python](https://img.shields.io/badge/python-3670A0?style=for-the-badge&logo=python&logoColor=ffdd54)
![DjangoREST](https://img.shields.io/badge/DJANGO-REST-ff1709?style=for-the-badge&logo=django&logoColor=white&color=ff1709&labelColor=gray)
![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)
Expand Down Expand Up @@ -29,7 +29,8 @@ TO-DO: Описание проекта.
| [drf-spectacular](https://drf-spectacular.readthedocs.io/en/latest/index.html) | Генератор документации и Swagger для API в Django. |
| [Djoser](https://pypi.org/project/djoser/) | Библиотека для обеспечения аутентификации в приложениях Django. |
| [Pillow](https://pypi.org/project/pillow/) | Библиотека для обработки изображений в Python. |
| [Django filter](https://pypi.org/project/django-filter/) | Библиотека для легкой фильтрации данных в приложениях Django. |
| [Django filter](https://pypi.org/project/django-filter/) | Библиотека для фильтрации данных в приложениях Django. |
| [Django Notifications](https://github.com/django-notifications/django-notifications) | Уведомления. |
| [Flake8](https://pypi.org/project/flake8/), [black](https://pypi.org/project/black/), [isort](https://pypi.org/project/isort/), [Pre-commit](https://pypi.org/project/pre-commit/) | Инструменты для поддержания Code-Style в проекте. |

## Code-Style
Expand Down
17 changes: 17 additions & 0 deletions api/v1/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from django.contrib.auth import get_user_model
from django_filters.rest_framework import BooleanFilter, FilterSet

User = get_user_model()


class CustomFilter(FilterSet):
no_ipr = BooleanFilter(method="filter_no_ipr", label="No IPR")

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

def filter_no_ipr(self, queryset, name, value):
if value:
return queryset.filter(ipr=None)
return queryset
31 changes: 31 additions & 0 deletions api/v1/serializers/api/notifications_serializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from notifications.models import Notification
from rest_framework import serializers

from api.v1.serializers.api.users_serializer import CustomUserSerializer


class NotificationSerializer(serializers.ModelSerializer):
"""Сериализатор уведомлений."""

recipient = CustomUserSerializer(read_only=True, many=False)
actor = CustomUserSerializer(read_only=True, many=False)

class Meta:
model = Notification
fields = (
"id",
"verb",
"unread",
"timestamp",
"recipient",
"actor",
)

# TODO: Добавить ссылку на объект уведомления.
# url = reverse('your_nested_detail_view_name',
# kwargs={'content_type': contentType, 'pk': objectId})
# >>> reverse("users-detail", args=[user.id])
# >>> '/api/v1/users/5/'
# request.build_absolute_uri('/some-relative-path/')
# т.е. всего 3 варианта, можно if-ами пройти.
# if isinstance(notification.target, IPR):
18 changes: 17 additions & 1 deletion api/v1/serializers/api/users_serializer.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
from django.contrib.auth import get_user_model
from djoser.serializers import UserSerializer
from rest_framework import serializers
from rest_framework.serializers import ModelSerializer

from users.models import User
from users.models import Position

User = get_user_model()


class PositionsSerializer(ModelSerializer):
"""Сериализатор должностей."""

class Meta:
model = Position
fields = ("name",)


class CustomUserSerializer(UserSerializer):
"""Сериализатор пользователей."""

position = serializers.CharField(source="position.name", read_only=True)

class Meta:
model = User
fields = (
Expand All @@ -20,4 +35,5 @@ class Meta:
"date_joined",
"last_login",
"userpic",
"team",
)
5 changes: 5 additions & 0 deletions api/v1/serializers/internal/dummy_serializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from rest_framework import serializers


class DummySerializer(serializers.Serializer):
pass
6 changes: 5 additions & 1 deletion api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@
from rest_framework import routers

from api.v1.views.ratings_view import IPRRatingCreateView, TaskRatingCreateView
from api.v1.views.notifications_view import NotificationViewSet
from api.v1.views.task import TaskViewSet
from api.v1.views.users_view import UserViewSet

v1_router = routers.DefaultRouter()
v1_router.register("users", UserViewSet, basename="users")
v1_router.register("tasks", TaskViewSet, basename="tasks")
v1_router.register("users", UserViewSet)
v1_router.register(
"notifications", NotificationViewSet, basename="notifications"
)

urlpatterns = [
path("", include(v1_router.urls)),
Expand Down
64 changes: 64 additions & 0 deletions api/v1/views/notifications_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response

from api.v1.serializers.api.notifications_serializer import (
NotificationSerializer,
)
from api.v1.serializers.internal.dummy_serializer import DummySerializer


@extend_schema(tags=["Уведомления"])
@extend_schema_view(
list=extend_schema(
summary="Список уведомлений",
description=(
"Список уведомлений, "
"уведомления именно для пользователя делающего запрос."
),
),
retrieve=extend_schema(
summary="Уведомление",
),
partial_update=extend_schema(
summary="Отметить уведомление как прочитанное",
description=(
"Чтобы отметить уведомление как прочитанное, "
"нужно изменить значение unread."
),
),
mark_all_as_read=extend_schema(
methods=["get"],
summary="Отметить все уведомления пользователя как прочтенные",
responses={status.HTTP_200_OK: DummySerializer},
),
)
class NotificationViewSet(viewsets.ModelViewSet):
serializer_class = NotificationSerializer
http_method_names = ["get", "patch", "head", "options"]

def partial_update(self, request, pk):
"""Отметить уведомление как прочитанное."""
notification = self.get_object()
if request.user != notification.recipient:
return Response(
{"message": "Уведомление не принадлежит пользователю."},
status=status.HTTP_403_FORBIDDEN,
)
notification.mark_as_read()
serializer = self.get_serializer(notification)
return Response(serializer.data)

def get_queryset(self):
"""Персонализированная выдача списка уведомлений."""
return self.request.user.notifications.unread()

@action(methods=["get"], detail=False)
def mark_all_as_read(self, request):
"""Отметить все уведомления пользователя как прочтенные."""
request.user.notifications.mark_all_as_read()
return Response(
{"message": "Уведомления отмечены как прочтенные."},
status=status.HTTP_200_OK,
)
88 changes: 81 additions & 7 deletions api/v1/views/users_view.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,90 @@
from django.conf import settings
from django.db.models import CharField, ExpressionWrapper, F, Value
from django_filters.rest_framework import DjangoFilterBackend
from djoser.views import UserViewSet as UserViewSetFromDjoser
from drf_spectacular.utils import extend_schema
from rest_framework.pagination import PageNumberPagination
from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import filters
from rest_framework.decorators import action
from rest_framework.filters import OrderingFilter
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.response import Response

from api.v1.serializers.api.users_serializer import CustomUserSerializer
from api.v1.filters import CustomFilter
from api.v1.serializers.api.users_serializer import (
CustomUserSerializer,
PositionsSerializer,
)
from users.models import Position


@extend_schema(
responses=CustomUserSerializer,
description="Пользователи.",
@extend_schema(tags=["Пользователи"])
@extend_schema_view(
list=extend_schema(
summary=("Список пользователей."),
description=(
"<ul><h3>Поддерживается:</h3><li>Сортировка по имени "
"<code>./?ordering=full_name</code> "
"и должности <code>./?ordering=-position_name</code></li>"
"<li>Поиск по ФИО и должности <code>./?search=Мирон</code></li>"
"<li>Ограничение pagination <code>./?limit=5</code>.</li>"
"<li>Фильтр по id команды <code>./?team=1</code></li>"
"<li>Фильтр по id команды и отсутствию ИПР "
"<code>./?team=1&no_ipr=true</code></li></ul>"
),
),
retrieve=extend_schema(summary="Профиль пользователя"),
me=extend_schema(summary="Текущий пользователь"),
)
class UserViewSet(UserViewSetFromDjoser):
"""Пользователи."""

filterset_class = CustomFilter
filter_backends = (
DjangoFilterBackend,
filters.SearchFilter,
OrderingFilter,
)
if settings.USE_POSTGRESQL: # TODO: Проверить.
search_fields = (
"@last_name",
"@first_name",
"@patronymic",
"@position__name",
)
else:
search_fields = (
"last_name",
"first_name",
"patronymic",
"position__name",
)
serializer_class = CustomUserSerializer
pagination_class = PageNumberPagination
pagination_class = LimitOffsetPagination
http_method_names = ["get", "head", "options"]
ordering_fields = ("full_name", "position_name")

def get_queryset(self):
queryset = super().get_queryset()

return queryset.annotate(
full_name=ExpressionWrapper(
F("last_name") + F("first_name") + F("patronymic"),
output_field=CharField(),
),
position_name=ExpressionWrapper(
F("position__name") if F("position__name") else Value(""),
output_field=CharField(),
),
)

@extend_schema(
summary="Список должностей",
)
@action(
methods=["get"], detail=False, serializer_class=PositionsSerializer
)
def positions(self, request):
"""Список должностей."""
queryset = Position.objects.all()
serializer = PositionsSerializer(queryset, many=True)
return Response(serializer.data)
21 changes: 11 additions & 10 deletions config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
env.read_env()

# ---------------------------------------------------------LOAD ENVIRONMENT VAR
SECRET_KEY = env.str("SECRET_KEY")
SECRET_KEY = env.str(
"SECRET_KEY",
default="django-insecure-pp6rzgeb5sjtbhfs(d-3*ibq67#0c-8jsd82@65!+=$satw167",
)
USE_POSTGRESQL = env.bool("USE_POSTGRESQL", default=False)
DEBUG = env.bool("DEBUG", default=False)
# -----------------------------------------------------------------------------
Expand All @@ -30,6 +33,7 @@
"djoser",
"django_filters",
"drf_spectacular",
"notifications",
]
LOCAL_APPS = [
"api.v1.apps.ApiConfig",
Expand All @@ -38,6 +42,7 @@
"tasks.apps.TasksConfig",
"users.apps.UsersConfig",
"ratings.apps.RatingsConfig",
"core.apps.CoreConfig",
]
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS

Expand Down Expand Up @@ -119,40 +124,35 @@

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# -------------------------------------------------------------CUSTOM SETTINGS

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

AUTH_USER_MODEL = "users.User"
# -----------------------------------------------------------------DRF SETTINGS

REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
"PAGE_SIZE": 6,
"PAGE_SIZE": 10,
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}

# Выключено предупреждение об отсутствие DEFAULT_PAGINATION_CLASS
SILENCED_SYSTEM_CHECKS = ["rest_framework.W001"]
# Время жизни токена увеличено, для упрощения тестирования.
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(days=1),
"AUTH_HEADER_TYPES": ("Bearer",),
}

# --------------------------------------------------------------DJOSER SETTINGS

DJOSER = {
"LOGIN_FIELD": "email",
# TODO: "PERMISSIONS": {},
"SERIALIZERS": {
"current_user": "api.v1.serializers.api.users_serializer.CustomUserSerializer",
},
"HIDE_USERS": False,
}

# -------------------------------------------------------------ALTER USER MODEL
SPECTACULAR_SETTINGS = {
"TITLE": "IPR API",
Expand All @@ -162,6 +162,7 @@
),
"VERSION": "0.1.0",
"SCHEMA_PATH_PREFIX": "/api/v1/",
"SERVE_INCLUDE_SCHEMA": False,
}
# --------------------------------------------------------------------CONSTANTS
EMAIL_LENGTH = 254
Expand Down
13 changes: 13 additions & 0 deletions core/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django.apps import AppConfig


class CoreConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "core"

def ready(self):
"""Добавление signals.py."""
try:
import core.signals # noqa
except ImportError:
pass
Loading

0 comments on commit 2e3154c

Please sign in to comment.