Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions docs/modules/feed-categories.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Контракт категорий ленты

`GET /feed/?type=project|vacancy|news&limit=6&offset=0` сохраняет стандартную
`GET /feed/?type=all_news&limit=6&offset=0` сохраняет стандартную
пагинацию `count`, `next`, `previous`, `results` и дополнительно возвращает
`counts` — количество доступных пользователю записей во всей ленте, независимо
от текущего фильтра и страницы:
Expand All @@ -9,7 +9,10 @@
{
"counts": {
"all": 4,
"all_news": 2,
"project": 2,
"project_news": 1,
"new_project": 1,
"vacancy": 1,
"news": 1,
"partnerprogram": 0,
Expand All @@ -18,10 +21,17 @@
}
```

`project` включает новости проектов и записи о новых проектах; `news` — новости
пользователей. Черновики, закрытые проекты и неактивные вакансии не учитываются,
как и в самой ленте. Новости программ и образование пока не включены в этот
endpoint, поэтому их счётчики равны нулю. `all` — сумма доступных категорий.
- `all_news` — публикации проектов и пользователей.
- `project_news` — только публикации проектов.
- `news` — только публикации пользователей.
- `vacancy` — активные вакансии.
- `new_project` — записи о новых проектах.

Черновики, закрытые проекты и неактивные вакансии не учитываются. Новости программ
и образование пока не включены в этот endpoint, поэтому их счётчики равны нулю.
Для совместимости `project` по-прежнему включает публикации и записи о новых
проектах, а `all` учитывает все видимые записи, включая вакансии. Фильтры можно
объединить через `|`.

Каждый элемент `results` теперь содержит `published_at` — дату публикации
записи ленты. Поле есть и у карточек новостей, и у вакансий, и у новых проектов.
Expand Down
35 changes: 35 additions & 0 deletions feed/tests/test_feed_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,38 @@ def test_feed_returns_project_feed_record_as_project_content(self):
self.get_feed_record(project).datetime_created,
)

def test_feed_separates_publications_from_new_projects_before_pagination(self):
project = create_project(name="Project with a publication")
project_news = create_news_for(project, text="Project publication")
user_news = create_news_for(self.user, text="Community publication")
new_project = create_project(name="Newest project without a publication")
vacancy = create_vacancy(project=project)
create_news_for(create_partner_program(name="Program"), text="Program publication")

cases = (
("all_news", [("news", user_news.pk), ("news", project_news.pk)]),
("project_news", [("news", project_news.pk)]),
("news", [("news", user_news.pk)]),
("vacancy", [("vacancy", vacancy.pk)]),
("new_project", [("project", new_project.pk), ("project", project.pk)]),
("partnerprogram", []),
("education", []),
)
for category, expected in cases:
with self.subTest(category=category):
received = []
for offset in range(max(len(expected), 1)):
response = self.client.get(
"/feed/", {"type": category, "limit": 1, "offset": offset}
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["count"], len(expected))
received.extend(
(item["type_model"], item["content"]["id"])
for item in response.data["results"]
)
self.assertEqual(received, expected)

def test_feed_returns_vacancy_feed_record_as_vacancy_content(self):
vacancy = create_vacancy(role="Backend developer")

Expand Down Expand Up @@ -129,7 +161,10 @@ def test_category_counts_are_global_and_respect_visibility(self):

expected_counts = {
"all": 5,
"all_news": 2,
"project": 2,
"project_news": 1,
"new_project": 1,
"vacancy": 2,
"news": 1,
"partnerprogram": 0,
Expand Down
45 changes: 32 additions & 13 deletions feed/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,36 @@
from feed.pagination import FeedPagination
from feed.services import get_liked_news
from news.models import News
from news.services import FEED_RECORD_TEXT
from projects.models import Project
from vacancy.models import Vacancy

from .serializers import FeedItemResponseSerializer


PROJECT_FEED = Q(content_type__model="project")
PROJECT_RECORD = Q(text=FEED_RECORD_TEXT)
PROJECT_NEWS = PROJECT_FEED & ~PROJECT_RECORD
NEW_PROJECTS = PROJECT_FEED & PROJECT_RECORD
COMMUNITY_NEWS = Q(content_type__model="customuser")
VACANCIES = Q(content_type__model="vacancy")

FEED_FILTERS = {
"all_news": PROJECT_NEWS | COMMUNITY_NEWS,
"project_news": PROJECT_NEWS,
"news": COMMUNITY_NEWS,
"vacancy": VACANCIES,
"new_project": NEW_PROJECTS,
# Existing clients still request the combined project category.
"project": PROJECT_FEED,
"customuser": COMMUNITY_NEWS,
}


class NewSimpleFeed(APIView):
serializer_class = FeedItemResponseSerializer
pagination_class = FeedPagination

def _get_filter_data(self) -> list[str]:
filter_queries: str | None = self.request.query_params.get("type")
filter_queries: str = filter_queries if filter_queries else "" # existence check

news_types: list[str] = filter_queries.split("|")
if "news" in news_types:
news_types.append("customuser")
return [news_type for news_type in news_types if news_type != "partnerprogram"]

def get_visible_queryset(self) -> QuerySet[News]:
queryset = News.objects.select_related("content_type").prefetch_related(
"content_object", "files"
Expand All @@ -46,21 +57,29 @@ def get_visible_queryset(self) -> QuerySet[News]:
return queryset

def get_queryset(self) -> QuerySet[News]:
filters = Q(pk__in=[])
for category in self.request.query_params.get("type", "").split("|"):
if category in FEED_FILTERS:
filters |= FEED_FILTERS[category]

return (
self.get_visible_queryset()
.filter(content_type__model__in=self._get_filter_data())
.filter(filters)
.order_by("-datetime_created", "-pk")
)

def get_category_counts(self) -> dict[str, int]:
# Программы и образование пока не включены в ленту; не считаем скрытый контент.
counts = self.get_visible_queryset().aggregate(
project=Count("pk", filter=Q(content_type__model="project")),
vacancy=Count("pk", filter=Q(content_type__model="vacancy")),
news=Count("pk", filter=Q(content_type__model="customuser")),
project_news=Count("pk", filter=PROJECT_NEWS),
new_project=Count("pk", filter=NEW_PROJECTS),
vacancy=Count("pk", filter=VACANCIES),
news=Count("pk", filter=COMMUNITY_NEWS),
)
return {
"all": sum(counts.values()),
"all_news": counts["project_news"] + counts["news"],
"project": counts["project_news"] + counts["new_project"],
**counts,
"partnerprogram": 0,
"education": 0,
Expand Down
Loading