Skip to content

Commit

Permalink
Merge pull request #293 from PROCOLLAB-github/dev
Browse files Browse the repository at this point in the history
Лента, оценки и куча куча багов
  • Loading branch information
Yakser authored Feb 22, 2024
2 parents 78f4c00 + 3fdaad1 commit da12c13
Show file tree
Hide file tree
Showing 19 changed files with 589 additions and 57 deletions.
19 changes: 17 additions & 2 deletions feed/constants.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import enum

from django.db.models import QuerySet
from rest_framework import serializers

from news.serializers import NewsListSerializer
from news.models import News
from news.serializers import NewsFeedListSerializer
from projects.models import Project
from projects.serializers import ProjectListSerializer
from vacancy.models import Vacancy
from vacancy.serializers import VacancyDetailSerializer


Expand All @@ -15,6 +19,17 @@ class FeedItemType(enum.Enum):

FEED_SERIALIZER_MAPPING: dict[FeedItemType, serializers.Serializer] = {
FeedItemType.PROJECT.value: ProjectListSerializer,
FeedItemType.NEWS.value: NewsListSerializer,
FeedItemType.NEWS.value: NewsFeedListSerializer,
FeedItemType.VACANCY.value: VacancyDetailSerializer,
}

SupportedModel = News | Project | Vacancy
SupportedQuerySet = QuerySet[News | Project | Vacancy]

model_mapping = {
FeedItemType.NEWS.value: News,
FeedItemType.PROJECT.value: Project,
FeedItemType.VACANCY.value: Vacancy,
}

LIMIT_PAGINATION_CONSTANT = 10
153 changes: 129 additions & 24 deletions feed/helpers.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,143 @@
import random
import typing
from random import shuffle
from typing import Iterable

from rest_framework.request import Request
from rest_framework.views import APIView

from feed import constants
from feed.constants import (
SupportedModel,
SupportedQuerySet,
LIMIT_PAGINATION_CONSTANT
)
from feed.pagination import FeedPagination
from feed.serializers import FeedItemSerializer
from news.models import News
from projects.models import Project

from django.db.models import Count

def collect_feed(models_list: typing.List, num) -> list[dict]:
get_model_data = {
model.__name__: collect_querysets(model, num) for model in models_list
}
from vacancy.models import Vacancy


def add_pagination(results: list[SupportedQuerySet], count: int) -> dict:
return {"count": count, "previous": None, "next": None, "results": results}


def paginate_serialize_feed(
model_data: dict[SupportedQuerySet],
paginator: FeedPagination,
request: Request,
view: APIView,
) -> tuple[list[SupportedQuerySet], int]:
result = []
for model in get_model_data:
result.extend(to_feed_items(model, get_model_data[model]))
random.shuffle(result)
return result
pages_count = 0

if len(model_data) == 0:
return [], 0

offset = request.query_params.get("offset", 0)
request.query_params._mutable = True

def collect_querysets(model, num):
if model.__name__ == Project.__class__.__name__:
return set(get_n_random_projects(num) + get_n_latest_created_projects(num))
if isinstance(offset, str) and offset.isdigit():
offset = int(offset)
else:
return list(model.objects.order_by("-datetime_created")[:num])
offset = 0

request.query_params["offset"] = offset

models_counts = {
model_name: model_data[model_name].count() for model_name in model_data.keys()
}
offset_numbers = offset_distribution(offset, models_counts)

for model_name in model_data.keys():
request.query_params["offset"] = offset_numbers[model_name]

paginated_part: dict = paginate_serialize_feed_queryset(
model_data, paginator, request, model_name, models_counts[model_name], view
)

result += paginated_part["paginated_data"]
pages_count += paginated_part["page_count"]

limit = request.query_params.get("limit", LIMIT_PAGINATION_CONSTANT)

if limit == "":
limit = LIMIT_PAGINATION_CONSTANT
else:
limit = int(limit)

shuffle(result)
return result[:limit], pages_count


def to_feed_items(type_: constants.FeedItemType, items: typing.Iterable) -> list[dict]:
def offset_distribution(offset: int, models_counts: dict) -> dict:
common_key_list = list(models_counts.keys())
quantity_of_models = len(list(models_counts.keys()))

full_division = offset // quantity_of_models
extra_items = offset % quantity_of_models

distributed_not_ready = {model_name: full_division for model_name in common_key_list}
distributed = dict(
sorted(distributed_not_ready.items(), key=lambda item: models_counts[item[0]])
)

last_key = common_key_list[-1]
distributed[last_key] += extra_items

new_keys_list = list(distributed.keys())
for i, key in enumerate(
new_keys_list
): # распределяем переполненные значения от маленьких моделей к большим
offset_value = distributed[key]
model_count = models_counts[key]
if offset_value > model_count:
diff = offset_value - model_count
distributed[key] = model_count
if i + 1 < len(new_keys_list):
next_key = new_keys_list[i + 1]
distributed[next_key] += diff

return distributed


def paginate_serialize_feed_queryset(
model_data: dict[SupportedQuerySet],
paginator: FeedPagination,
request: Request,
model: SupportedModel,
count: int,
view: APIView,
) -> dict:
paginated_info = paginator.custom_paginate_queryset(
model_data[model], request, count, view=view
)
paginated_data = paginated_info["queryset_ready"]
num_pages = paginated_info["count"]
return {
"paginated_data": to_feed_items(model, paginated_data),
"page_count": num_pages,
}


def collect_querysets(model: SupportedModel) -> SupportedQuerySet:
if model == Project:
queryset = model.objects.select_related("leader", "industry").filter(draft=False)
elif model == Vacancy:
queryset = model.objects.select_related("project")
elif model == News:
queryset = (
model.objects.select_related("content_type")
.prefetch_related("content_object", "files")
.annotate(likes_count=Count("likes"), views_count=Count("views"))
)

return queryset.order_by("-datetime_created")


def to_feed_items(type_: constants.FeedItemType, items: Iterable) -> list[dict]:
feed_items = []
for item in items:
serializer = to_feed_item(type_, item)
Expand All @@ -33,14 +146,6 @@ def to_feed_items(type_: constants.FeedItemType, items: typing.Iterable) -> list
return feed_items


def get_n_random_projects(num: int) -> list[Project]:
return list(Project.objects.filter(draft=False).order_by("?").distinct()[:num])


def get_n_latest_created_projects(num: int) -> list[Project]:
return list(Project.objects.filter(draft=False).order_by("-datetime_created")[:num])


def to_feed_item(type_: constants.FeedItemType, data):
serializer = constants.FEED_SERIALIZER_MAPPING[type_](data)
return FeedItemSerializer(data={"type": type_, "content": serializer.data})
return FeedItemSerializer(data={"type_model": type_, "content": serializer.data})
32 changes: 32 additions & 0 deletions feed/pagination.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from rest_framework import pagination
from rest_framework.request import Request

from feed.constants import SupportedQuerySet


class FeedPagination(pagination.LimitOffsetPagination):
default_limit = 10
limit_query_param = "limit"
offset_query_param = "offset"

def custom_paginate_queryset(
self, queryset: SupportedQuerySet, request: Request, count: int, view=None
) -> dict:
self.limit = self.get_limit(request)
if self.limit is None:
return None

self.count = count
self.offset = self.get_offset(request)
self.request = request
if self.count > self.limit and self.template is not None:
self.display_page_controls = True

if self.count == 0 or self.offset > self.count:
return {"queryset_ready": [], "count": self.count}

queryset_ready = queryset[self.offset : self.offset + self.limit] # noqa: E203
return {
"queryset_ready": queryset_ready,
"count": self.count,
}
2 changes: 1 addition & 1 deletion feed/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@


class FeedItemSerializer(serializers.Serializer):
type = serializers.ChoiceField(choices=constants.FeedItemType, required=True)
type_model = serializers.ChoiceField(choices=constants.FeedItemType, required=True)
content = serializers.JSONField(required=True)
50 changes: 36 additions & 14 deletions feed/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
from rest_framework.response import Response
from rest_framework.views import APIView

from feed.helpers import collect_feed
from news.models import News
from projects.models import Project
from vacancy.models import Vacancy
from feed.constants import SupportedModel, SupportedQuerySet, FeedItemType, model_mapping
from feed.helpers import collect_querysets, paginate_serialize_feed, add_pagination
from feed.pagination import FeedPagination


class FeedList(APIView):
pagination_class = FeedPagination

@swagger_auto_schema(
responses={
200: openapi.Response(
Expand All @@ -31,13 +32,34 @@ class FeedList(APIView):
}
)
def get(self, request: Request, *args, **kwargs) -> Response:
models = []
filter = request.query_params.get("type")
if "news" in filter:
models.append(News)
if "project" in filter:
models.append(Project)
if "vacancy" in filter:
models.append(Vacancy)

return Response(status=status.HTTP_200_OK, data=collect_feed(models, 3))
prepared_data, sum_pages = self.paginate_serialize_data(
self.get_response_data(self.get_request_data())
)
for obj in prepared_data:
obj["type_model"] = obj["type_model"].lower()
return Response(
status=status.HTTP_200_OK,
data=add_pagination(prepared_data, sum_pages),
)

def get_request_data(self) -> list[SupportedModel]:
filter_queries = self.request.query_params.get("type")
filter_queries = filter_queries if filter_queries else "" # existence check

models = [
model_mapping[model_name]
for model_name in model_mapping.keys()
if model_name.lower() in filter_queries
]
return models

def get_response_data(
self, models: list[SupportedModel]
) -> dict[FeedItemType, SupportedQuerySet]:
return {model.__name__: collect_querysets(model) for model in models}

def paginate_serialize_data(
self, get_model_data: dict[FeedItemType, SupportedQuerySet]
) -> tuple[list[dict], int]:
paginator = self.pagination_class()
return paginate_serialize_feed(get_model_data, paginator, self.request, self)
55 changes: 55 additions & 0 deletions news/serializers.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
from django.contrib.auth import get_user_model
from django.forms import model_to_dict
from rest_framework import serializers

from core.services import is_fan, get_likes_count, get_views_count
from files.serializers import UserFileSerializer
from news.mapping import NewsMapping
from news.models import News
from projects.models import Project
from projects.serializers import ProjectListSerializer
from users.models import CustomUser
from users.serializers import UserFeedSerializer

User = get_user_model()

Expand Down Expand Up @@ -59,6 +64,56 @@ class Meta:
]


class NewsFeedListSerializer(serializers.ModelSerializer):
name = serializers.SerializerMethodField()
image_address = serializers.SerializerMethodField()
is_user_liked = serializers.SerializerMethodField()
files = UserFileSerializer(many=True)
views_count = serializers.IntegerField(default=0)
likes_count = serializers.IntegerField(default=0)
content_object = serializers.SerializerMethodField()

def get_content_object(self, obj):
if obj.content_type.model == Project.__name__.lower():
serialized_obj = ProjectListSerializer(instance=obj.content_object, data={})
serialized_obj.is_valid()
return serialized_obj.data
elif obj.content_type.model == CustomUser.__name__.lower():
serialized_obj = UserFeedSerializer(
instance=obj.content_object, data=model_to_dict(obj.content_object)
)
serialized_obj.is_valid()
return serialized_obj.data

def get_name(self, obj):
return NewsMapping.get_name(obj.content_object)

def get_image_address(self, obj):
return NewsMapping.get_image_address(obj.content_object)

def get_is_user_liked(self, obj):
user = self.context.get("user")
if user:
return is_fan(obj, user)
return False

class Meta:
model = News
fields = [
"id",
"name",
"image_address",
"text",
"datetime_created",
"views_count",
"likes_count",
"files",
"is_user_liked",
"content_object",
]
read_only_fields = ["views_count", "likes_count"]


class NewsDetailSerializer(serializers.ModelSerializer):
views_count = serializers.SerializerMethodField()
likes_count = serializers.SerializerMethodField()
Expand Down
Loading

0 comments on commit da12c13

Please sign in to comment.