Skip to content

Commit da12c13

Browse files
authored
Merge pull request #293 from PROCOLLAB-github/dev
Лента, оценки и куча куча багов
2 parents 78f4c00 + 3fdaad1 commit da12c13

19 files changed

Lines changed: 589 additions & 57 deletions

feed/constants.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import enum
22

3+
from django.db.models import QuerySet
34
from rest_framework import serializers
45

5-
from news.serializers import NewsListSerializer
6+
from news.models import News
7+
from news.serializers import NewsFeedListSerializer
8+
from projects.models import Project
69
from projects.serializers import ProjectListSerializer
10+
from vacancy.models import Vacancy
711
from vacancy.serializers import VacancyDetailSerializer
812

913

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

1620
FEED_SERIALIZER_MAPPING: dict[FeedItemType, serializers.Serializer] = {
1721
FeedItemType.PROJECT.value: ProjectListSerializer,
18-
FeedItemType.NEWS.value: NewsListSerializer,
22+
FeedItemType.NEWS.value: NewsFeedListSerializer,
1923
FeedItemType.VACANCY.value: VacancyDetailSerializer,
2024
}
25+
26+
SupportedModel = News | Project | Vacancy
27+
SupportedQuerySet = QuerySet[News | Project | Vacancy]
28+
29+
model_mapping = {
30+
FeedItemType.NEWS.value: News,
31+
FeedItemType.PROJECT.value: Project,
32+
FeedItemType.VACANCY.value: Vacancy,
33+
}
34+
35+
LIMIT_PAGINATION_CONSTANT = 10

feed/helpers.py

Lines changed: 129 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,143 @@
1-
import random
2-
import typing
1+
from random import shuffle
2+
from typing import Iterable
3+
4+
from rest_framework.request import Request
5+
from rest_framework.views import APIView
36

47
from feed import constants
8+
from feed.constants import (
9+
SupportedModel,
10+
SupportedQuerySet,
11+
LIMIT_PAGINATION_CONSTANT
12+
)
13+
from feed.pagination import FeedPagination
514
from feed.serializers import FeedItemSerializer
15+
from news.models import News
616
from projects.models import Project
717

18+
from django.db.models import Count
819

9-
def collect_feed(models_list: typing.List, num) -> list[dict]:
10-
get_model_data = {
11-
model.__name__: collect_querysets(model, num) for model in models_list
12-
}
20+
from vacancy.models import Vacancy
21+
22+
23+
def add_pagination(results: list[SupportedQuerySet], count: int) -> dict:
24+
return {"count": count, "previous": None, "next": None, "results": results}
25+
26+
27+
def paginate_serialize_feed(
28+
model_data: dict[SupportedQuerySet],
29+
paginator: FeedPagination,
30+
request: Request,
31+
view: APIView,
32+
) -> tuple[list[SupportedQuerySet], int]:
1333
result = []
14-
for model in get_model_data:
15-
result.extend(to_feed_items(model, get_model_data[model]))
16-
random.shuffle(result)
17-
return result
34+
pages_count = 0
35+
36+
if len(model_data) == 0:
37+
return [], 0
1838

39+
offset = request.query_params.get("offset", 0)
40+
request.query_params._mutable = True
1941

20-
def collect_querysets(model, num):
21-
if model.__name__ == Project.__class__.__name__:
22-
return set(get_n_random_projects(num) + get_n_latest_created_projects(num))
42+
if isinstance(offset, str) and offset.isdigit():
43+
offset = int(offset)
2344
else:
24-
return list(model.objects.order_by("-datetime_created")[:num])
45+
offset = 0
46+
47+
request.query_params["offset"] = offset
48+
49+
models_counts = {
50+
model_name: model_data[model_name].count() for model_name in model_data.keys()
51+
}
52+
offset_numbers = offset_distribution(offset, models_counts)
53+
54+
for model_name in model_data.keys():
55+
request.query_params["offset"] = offset_numbers[model_name]
56+
57+
paginated_part: dict = paginate_serialize_feed_queryset(
58+
model_data, paginator, request, model_name, models_counts[model_name], view
59+
)
60+
61+
result += paginated_part["paginated_data"]
62+
pages_count += paginated_part["page_count"]
63+
64+
limit = request.query_params.get("limit", LIMIT_PAGINATION_CONSTANT)
65+
66+
if limit == "":
67+
limit = LIMIT_PAGINATION_CONSTANT
68+
else:
69+
limit = int(limit)
70+
71+
shuffle(result)
72+
return result[:limit], pages_count
2573

2674

27-
def to_feed_items(type_: constants.FeedItemType, items: typing.Iterable) -> list[dict]:
75+
def offset_distribution(offset: int, models_counts: dict) -> dict:
76+
common_key_list = list(models_counts.keys())
77+
quantity_of_models = len(list(models_counts.keys()))
78+
79+
full_division = offset // quantity_of_models
80+
extra_items = offset % quantity_of_models
81+
82+
distributed_not_ready = {model_name: full_division for model_name in common_key_list}
83+
distributed = dict(
84+
sorted(distributed_not_ready.items(), key=lambda item: models_counts[item[0]])
85+
)
86+
87+
last_key = common_key_list[-1]
88+
distributed[last_key] += extra_items
89+
90+
new_keys_list = list(distributed.keys())
91+
for i, key in enumerate(
92+
new_keys_list
93+
): # распределяем переполненные значения от маленьких моделей к большим
94+
offset_value = distributed[key]
95+
model_count = models_counts[key]
96+
if offset_value > model_count:
97+
diff = offset_value - model_count
98+
distributed[key] = model_count
99+
if i + 1 < len(new_keys_list):
100+
next_key = new_keys_list[i + 1]
101+
distributed[next_key] += diff
102+
103+
return distributed
104+
105+
106+
def paginate_serialize_feed_queryset(
107+
model_data: dict[SupportedQuerySet],
108+
paginator: FeedPagination,
109+
request: Request,
110+
model: SupportedModel,
111+
count: int,
112+
view: APIView,
113+
) -> dict:
114+
paginated_info = paginator.custom_paginate_queryset(
115+
model_data[model], request, count, view=view
116+
)
117+
paginated_data = paginated_info["queryset_ready"]
118+
num_pages = paginated_info["count"]
119+
return {
120+
"paginated_data": to_feed_items(model, paginated_data),
121+
"page_count": num_pages,
122+
}
123+
124+
125+
def collect_querysets(model: SupportedModel) -> SupportedQuerySet:
126+
if model == Project:
127+
queryset = model.objects.select_related("leader", "industry").filter(draft=False)
128+
elif model == Vacancy:
129+
queryset = model.objects.select_related("project")
130+
elif model == News:
131+
queryset = (
132+
model.objects.select_related("content_type")
133+
.prefetch_related("content_object", "files")
134+
.annotate(likes_count=Count("likes"), views_count=Count("views"))
135+
)
136+
137+
return queryset.order_by("-datetime_created")
138+
139+
140+
def to_feed_items(type_: constants.FeedItemType, items: Iterable) -> list[dict]:
28141
feed_items = []
29142
for item in items:
30143
serializer = to_feed_item(type_, item)
@@ -33,14 +146,6 @@ def to_feed_items(type_: constants.FeedItemType, items: typing.Iterable) -> list
33146
return feed_items
34147

35148

36-
def get_n_random_projects(num: int) -> list[Project]:
37-
return list(Project.objects.filter(draft=False).order_by("?").distinct()[:num])
38-
39-
40-
def get_n_latest_created_projects(num: int) -> list[Project]:
41-
return list(Project.objects.filter(draft=False).order_by("-datetime_created")[:num])
42-
43-
44149
def to_feed_item(type_: constants.FeedItemType, data):
45150
serializer = constants.FEED_SERIALIZER_MAPPING[type_](data)
46-
return FeedItemSerializer(data={"type": type_, "content": serializer.data})
151+
return FeedItemSerializer(data={"type_model": type_, "content": serializer.data})

feed/pagination.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from rest_framework import pagination
2+
from rest_framework.request import Request
3+
4+
from feed.constants import SupportedQuerySet
5+
6+
7+
class FeedPagination(pagination.LimitOffsetPagination):
8+
default_limit = 10
9+
limit_query_param = "limit"
10+
offset_query_param = "offset"
11+
12+
def custom_paginate_queryset(
13+
self, queryset: SupportedQuerySet, request: Request, count: int, view=None
14+
) -> dict:
15+
self.limit = self.get_limit(request)
16+
if self.limit is None:
17+
return None
18+
19+
self.count = count
20+
self.offset = self.get_offset(request)
21+
self.request = request
22+
if self.count > self.limit and self.template is not None:
23+
self.display_page_controls = True
24+
25+
if self.count == 0 or self.offset > self.count:
26+
return {"queryset_ready": [], "count": self.count}
27+
28+
queryset_ready = queryset[self.offset : self.offset + self.limit] # noqa: E203
29+
return {
30+
"queryset_ready": queryset_ready,
31+
"count": self.count,
32+
}

feed/serializers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33

44

55
class FeedItemSerializer(serializers.Serializer):
6-
type = serializers.ChoiceField(choices=constants.FeedItemType, required=True)
6+
type_model = serializers.ChoiceField(choices=constants.FeedItemType, required=True)
77
content = serializers.JSONField(required=True)

feed/views.py

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
from rest_framework.response import Response
66
from rest_framework.views import APIView
77

8-
from feed.helpers import collect_feed
9-
from news.models import News
10-
from projects.models import Project
11-
from vacancy.models import Vacancy
8+
from feed.constants import SupportedModel, SupportedQuerySet, FeedItemType, model_mapping
9+
from feed.helpers import collect_querysets, paginate_serialize_feed, add_pagination
10+
from feed.pagination import FeedPagination
1211

1312

1413
class FeedList(APIView):
14+
pagination_class = FeedPagination
15+
1516
@swagger_auto_schema(
1617
responses={
1718
200: openapi.Response(
@@ -31,13 +32,34 @@ class FeedList(APIView):
3132
}
3233
)
3334
def get(self, request: Request, *args, **kwargs) -> Response:
34-
models = []
35-
filter = request.query_params.get("type")
36-
if "news" in filter:
37-
models.append(News)
38-
if "project" in filter:
39-
models.append(Project)
40-
if "vacancy" in filter:
41-
models.append(Vacancy)
42-
43-
return Response(status=status.HTTP_200_OK, data=collect_feed(models, 3))
35+
prepared_data, sum_pages = self.paginate_serialize_data(
36+
self.get_response_data(self.get_request_data())
37+
)
38+
for obj in prepared_data:
39+
obj["type_model"] = obj["type_model"].lower()
40+
return Response(
41+
status=status.HTTP_200_OK,
42+
data=add_pagination(prepared_data, sum_pages),
43+
)
44+
45+
def get_request_data(self) -> list[SupportedModel]:
46+
filter_queries = self.request.query_params.get("type")
47+
filter_queries = filter_queries if filter_queries else "" # existence check
48+
49+
models = [
50+
model_mapping[model_name]
51+
for model_name in model_mapping.keys()
52+
if model_name.lower() in filter_queries
53+
]
54+
return models
55+
56+
def get_response_data(
57+
self, models: list[SupportedModel]
58+
) -> dict[FeedItemType, SupportedQuerySet]:
59+
return {model.__name__: collect_querysets(model) for model in models}
60+
61+
def paginate_serialize_data(
62+
self, get_model_data: dict[FeedItemType, SupportedQuerySet]
63+
) -> tuple[list[dict], int]:
64+
paginator = self.pagination_class()
65+
return paginate_serialize_feed(get_model_data, paginator, self.request, self)

news/serializers.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
from django.contrib.auth import get_user_model
2+
from django.forms import model_to_dict
23
from rest_framework import serializers
34

45
from core.services import is_fan, get_likes_count, get_views_count
56
from files.serializers import UserFileSerializer
67
from news.mapping import NewsMapping
78
from news.models import News
9+
from projects.models import Project
10+
from projects.serializers import ProjectListSerializer
11+
from users.models import CustomUser
12+
from users.serializers import UserFeedSerializer
813

914
User = get_user_model()
1015

@@ -59,6 +64,56 @@ class Meta:
5964
]
6065

6166

67+
class NewsFeedListSerializer(serializers.ModelSerializer):
68+
name = serializers.SerializerMethodField()
69+
image_address = serializers.SerializerMethodField()
70+
is_user_liked = serializers.SerializerMethodField()
71+
files = UserFileSerializer(many=True)
72+
views_count = serializers.IntegerField(default=0)
73+
likes_count = serializers.IntegerField(default=0)
74+
content_object = serializers.SerializerMethodField()
75+
76+
def get_content_object(self, obj):
77+
if obj.content_type.model == Project.__name__.lower():
78+
serialized_obj = ProjectListSerializer(instance=obj.content_object, data={})
79+
serialized_obj.is_valid()
80+
return serialized_obj.data
81+
elif obj.content_type.model == CustomUser.__name__.lower():
82+
serialized_obj = UserFeedSerializer(
83+
instance=obj.content_object, data=model_to_dict(obj.content_object)
84+
)
85+
serialized_obj.is_valid()
86+
return serialized_obj.data
87+
88+
def get_name(self, obj):
89+
return NewsMapping.get_name(obj.content_object)
90+
91+
def get_image_address(self, obj):
92+
return NewsMapping.get_image_address(obj.content_object)
93+
94+
def get_is_user_liked(self, obj):
95+
user = self.context.get("user")
96+
if user:
97+
return is_fan(obj, user)
98+
return False
99+
100+
class Meta:
101+
model = News
102+
fields = [
103+
"id",
104+
"name",
105+
"image_address",
106+
"text",
107+
"datetime_created",
108+
"views_count",
109+
"likes_count",
110+
"files",
111+
"is_user_liked",
112+
"content_object",
113+
]
114+
read_only_fields = ["views_count", "likes_count"]
115+
116+
62117
class NewsDetailSerializer(serializers.ModelSerializer):
63118
views_count = serializers.SerializerMethodField()
64119
likes_count = serializers.SerializerMethodField()

0 commit comments

Comments
 (0)