diff --git a/docs/modules/feed-categories.md b/docs/modules/feed-categories.md index 0173faa8..23909ed4 100644 --- a/docs/modules/feed-categories.md +++ b/docs/modules/feed-categories.md @@ -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` — количество доступных пользователю записей во всей ленте, независимо от текущего фильтра и страницы: @@ -9,7 +9,10 @@ { "counts": { "all": 4, + "all_news": 2, "project": 2, + "project_news": 1, + "new_project": 1, "vacancy": 1, "news": 1, "partnerprogram": 0, @@ -18,10 +21,17 @@ } ``` -`project` включает новости проектов и записи о новых проектах; `news` — новости -пользователей. Черновики, закрытые проекты и неактивные вакансии не учитываются, -как и в самой ленте. Новости программ и образование пока не включены в этот -endpoint, поэтому их счётчики равны нулю. `all` — сумма доступных категорий. +- `all_news` — публикации проектов и пользователей. +- `project_news` — только публикации проектов. +- `news` — только публикации пользователей. +- `vacancy` — активные вакансии. +- `new_project` — записи о новых проектах. + +Черновики, закрытые проекты и неактивные вакансии не учитываются. Новости программ +и образование пока не включены в этот endpoint, поэтому их счётчики равны нулю. +Для совместимости `project` по-прежнему включает публикации и записи о новых +проектах, а `all` учитывает все видимые записи, включая вакансии. Фильтры можно +объединить через `|`. Каждый элемент `results` теперь содержит `published_at` — дату публикации записи ленты. Поле есть и у карточек новостей, и у вакансий, и у новых проектов. diff --git a/feed/tests/test_feed_api.py b/feed/tests/test_feed_api.py index fdc3f866..96e681ad 100644 --- a/feed/tests/test_feed_api.py +++ b/feed/tests/test_feed_api.py @@ -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") @@ -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, diff --git a/feed/views.py b/feed/views.py index afcca516..f972e220 100644 --- a/feed/views.py +++ b/feed/views.py @@ -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" @@ -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,