Skip to content

Commit cbc485b

Browse files
committed
Рефакторинг модуля News и удаление legacy ProjectNews
1 parent 8495994 commit cbc485b

26 files changed

Lines changed: 391 additions & 682 deletions

docs/modules/feed.md

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,90 @@
11
# Feed
22

3-
TODO
3+
## Назначение
4+
5+
Feed отвечает за общую ленту `/feed/`.
6+
7+
Модуль не хранит отдельную доменную модель ленты. Он читает записи из
8+
`news.News` и возвращает их во frontend-формате, зависящем от связанного
9+
объекта.
10+
11+
## Статус модуля
12+
13+
Модуль рабочий, но небольшой и связан с `news`, `projects` и `vacancy`.
14+
Основная логика сосредоточена во view, serializer и service helpers.
15+
16+
## Основные возможности
17+
18+
- сбор общей ленты по query-параметру `type`;
19+
- отображение пользовательских новостей;
20+
- отображение проектных новостей;
21+
- отображение служебных записей для проектов и вакансий;
22+
- исключение записей непубличных или черновых проектов;
23+
- передача признака лайка текущим пользователем.
24+
25+
## Архитектура
26+
27+
- `feed/views.py` - endpoint `/feed/`, фильтрация по типам и финальная сборка
28+
response payload.
29+
- `feed/serializers.py` - `FeedNewsResponseSerializer`, который превращает
30+
`news.News` в элемент ленты.
31+
- `feed/services.py` - helpers для лайков и служебных feed-записей.
32+
- `feed/mapping.py` - соответствие content object типам и serializers.
33+
- `feed/constants.py` - типы моделей, для которых signals создают feed-записи.
34+
- `feed/signals.py` - подключение signal handlers.
35+
- `feed/tests/` - regression-тесты API и service helpers.
36+
37+
## Основные сценарии
38+
39+
### 1. Пользователь открывает ленту
40+
41+
Frontend вызывает `/feed/?type=...`.
42+
View выбирает подходящие `news.News`, сериализует их и возвращает элементы
43+
ленты с полями `type_model` и `content`.
44+
45+
### 2. В ленту попадает обычная новость
46+
47+
Если `news.News` содержит текст и относится к пользователю или проекту, лента
48+
возвращает ее как новость.
49+
50+
Проектная новость с текстом возвращается как `type_model = "news"`, даже если
51+
ее `content_object` - проект.
52+
53+
### 3. В ленту попадает служебная запись
54+
55+
Служебные feed-записи создаются через `feed.services.create_news_for_model()`.
56+
Они используют `news.News` с пустым `text` и связью на объект, например проект
57+
или вакансию.
58+
59+
### 4. Проект становится недоступным для публичной ленты
60+
61+
Если проект черновой или непубличный, связанные с ним записи не возвращаются в
62+
`/feed/`.
63+
64+
## API
65+
66+
- `GET /feed/?type=news` - новости пользователей.
67+
- `GET /feed/?type=project` - проектные новости и проектные feed-записи.
68+
- `GET /feed/?type=project|news` - комбинированная выдача по нескольким типам.
69+
70+
## Ограничения и правила
71+
72+
- Feed читает данные из `news.News`, но не отвечает за создание обычных
73+
project/user/program news.
74+
- Служебная feed-запись определяется через пустой `text`.
75+
- Signals проектов могут создавать или удалять feed-записи, но тесты этих
76+
side effects остаются в модуле `projects`.
77+
- `DevScript` в `feed/views.py` остается служебным legacy-инструментом и не
78+
является основным пользовательским API.
79+
80+
## Тесты
81+
82+
Текущие regression-тесты проверяют:
83+
84+
- `/feed/?type=news` возвращает пользовательские новости;
85+
- `/feed/?type=project` возвращает проектные новости в frontend-формате;
86+
- новости непубличных проектов не попадают в feed;
87+
- `get_liked_news()` возвращает лайкнутые текущим пользователем записи;
88+
- `create_news_for_model()` создает одну служебную feed-запись без дублей;
89+
- `delete_news_for_model()` удаляет только служебную feed-запись и не трогает
90+
обычную новость с текстом.

docs/modules/news.md

Lines changed: 43 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ generic relation:
1212
- партнерскими программами;
1313
- объектами ленты, например вакансиями.
1414

15-
Одна и та же модель используется для двух близких, но разных сценариев:
15+
Одна и та же модель используется для двух сценариев:
1616

1717
- обычная новость с текстом и файлами;
1818
- служебная запись ленты для существующего объекта, где `text = ""`.
@@ -23,7 +23,7 @@ generic relation:
2323
аккуратного рефакторинга. Сейчас он обслуживает проектные новости, новости
2424
пользователей, новости программ и часть общей ленты.
2525

26-
Первый слой regression-тестов добавлен для живых сценариев API и feed.
26+
Первый слой regression-тестов добавлен для живых сценариев API.
2727

2828
## Основные возможности
2929

@@ -41,14 +41,16 @@ generic relation:
4141

4242
- `news/models.py` - модель `News` с `content_type/object_id`, файлами, лайками,
4343
просмотрами и флагом `pin`.
44-
- `news/managers.py` - `get_news(obj)` и `add_news(obj, **kwargs)` для работы с
45-
generic relation.
46-
- `news/mixins.py` - выбор queryset по контексту URL: project, user или partner
47-
program.
44+
- `news/managers.py` - низкоуровневые `get_news(obj)` и `add_news(obj,
45+
**kwargs)` для работы с generic relation.
46+
- `news/services.py` - явное создание project/user/program news и helpers для
47+
различения обычной новости и feed-записи.
48+
- `news/querysets.py` - явные queryset helpers по контексту URL: project, user
49+
или partner program.
4850
- `news/views.py` - общий API для list/create/detail/update/delete, set_viewed и
4951
set_liked.
50-
- `news/serializers.py` - request/response serializers для списка, detail и feed
51-
представления.
52+
- `news/serializers.py` - request/response serializers для создания, списка и
53+
detail.
5254
- `news/permissions.py` - права на создание и изменение новости в зависимости от
5355
связанного объекта.
5456
- `news/admin.py` - админка `News`.
@@ -63,45 +65,29 @@ generic relation:
6365
- `views` - generic views через `core.View`.
6466
- `pin` - закрепление новости, сейчас используется для новостей программ.
6567

68+
Feed-запись определяется через helper `is_feed_record(news)`, а обычная новость
69+
через `is_content_news(news)`. Сейчас оба helper'а используют текущий признак
70+
`text`, но вызывающий код не должен напрямую проверять `text == ""`.
71+
6672
## API
6773

68-
Контекстные endpoints:
69-
70-
- `GET /projects/<project_id>/news/` - список новостей проекта.
71-
- `POST /projects/<project_id>/news/` - создание новости проекта.
72-
- `GET /projects/<project_id>/news/<news_id>/` - детальная новость проекта.
73-
- `PATCH /projects/<project_id>/news/<news_id>/` - редактирование новости
74-
проекта.
75-
- `DELETE /projects/<project_id>/news/<news_id>/` - удаление новости проекта.
76-
- `POST /projects/<project_id>/news/<news_id>/set_viewed/` - просмотр новости
77-
проекта.
78-
- `POST /projects/<project_id>/news/<news_id>/set_liked/` - лайк новости
79-
проекта.
80-
81-
- `GET /auth/users/<user_id>/news/` - список новостей пользователя.
82-
- `POST /auth/users/<user_id>/news/` - создание новости пользователя.
83-
- `GET /auth/users/<user_id>/news/<news_id>/` - детальная новость пользователя.
84-
- `PATCH /auth/users/<user_id>/news/<news_id>/` - редактирование новости
85-
пользователя.
86-
- `DELETE /auth/users/<user_id>/news/<news_id>/` - удаление новости
87-
пользователя.
88-
- `POST /auth/users/<user_id>/news/<news_id>/set_viewed/` - просмотр новости
89-
пользователя.
90-
- `POST /auth/users/<user_id>/news/<news_id>/set_liked/` - лайк новости
91-
пользователя.
92-
93-
- `GET /programs/<program_id>/news/` - список новостей программы.
94-
- `POST /programs/<program_id>/news/` - создание новости программы.
95-
- `GET /programs/<program_id>/news/<news_id>/` - детальная новость программы.
96-
- `PATCH /programs/<program_id>/news/<news_id>/` - редактирование новости
97-
программы.
98-
- `DELETE /programs/<program_id>/news/<news_id>/` - удаление новости программы.
99-
- `POST /programs/<program_id>/news/<news_id>/set_viewed/` - просмотр новости
100-
программы.
101-
- `POST /programs/<program_id>/news/<news_id>/set_liked/` - лайк новости
102-
программы.
103-
104-
Общие endpoints:
74+
Контекстные endpoints работают для трех базовых URL:
75+
76+
- `/projects/<project_id>/news/` - новости проекта;
77+
- `/auth/users/<user_id>/news/` - новости пользователя;
78+
- `/programs/<program_id>/news/` - новости партнерской программы.
79+
80+
Для каждого контекста доступны:
81+
82+
- `GET <base>` - список новостей;
83+
- `POST <base>` - создание новости;
84+
- `GET <base><news_id>/` - детальная новость;
85+
- `PATCH <base><news_id>/` - редактирование новости;
86+
- `DELETE <base><news_id>/` - удаление новости;
87+
- `POST <base><news_id>/set_viewed/` - просмотр новости;
88+
- `POST <base><news_id>/set_liked/` - лайк новости.
89+
90+
Связанные endpoints:
10591

10692
- `GET /news/` - подключен напрямую, но без контекста возвращает пустой список.
10793
- `GET /news/<news_id>/` - подключен напрямую, но без контекста не является
@@ -117,9 +103,8 @@ generic relation:
117103
Новость сохраняется в `news.News`, а связь с проектом задается через
118104
`content_type = Project` и `object_id = project.id`.
119105

120-
Новости проекта с непустым `text` отображаются как новости внутри проекта.
121-
Служебные feed-записи проекта с `text = ""` из списка проектных новостей
122-
исключаются.
106+
Новости проекта с текстом отображаются внутри проекта. Служебные feed-записи
107+
из списка проектных новостей исключаются.
123108

124109
### 2. Новость пользователя
125110

@@ -148,8 +133,8 @@ generic relation:
148133
Для проектных записей важно различать:
149134

150135
- `text = ""` - служебная запись ленты о проекте;
151-
- `text != ""` - полноценная новость проекта, которая в ленте возвращается как
152-
`type_model = "news"`.
136+
- новость с текстом - полноценная новость проекта, которая в ленте возвращается
137+
как `type_model = "news"`.
153138

154139
Лента исключает новости, связанные с непубличными или черновыми проектами.
155140

@@ -160,23 +145,16 @@ generic relation:
160145
- Новость программы может создавать и изменять только менеджер программы.
161146
- Прямой `/news/` без project/user/program context не является основным
162147
пользовательским API.
163-
- Старые `ProjectNews*` в `projects` не являются текущей реализацией проектных
164-
новостей; живые routes используют `news.News`.
148+
- Несуществующий project/user/program context возвращает `404`.
149+
- Проектные новости реализованы через `news.News`.
165150

166151
## Тесты
167152

168153
Текущие regression-тесты проверяют:
169154

170-
- `NewsManager.add_news()` привязывает новость к content object и файлам;
171-
- `NewsManager.get_news()` возвращает новости нужного объекта;
172-
- лидер проекта может создавать, редактировать и удалять новости проекта;
173-
- пользователь без роли лидера не может создавать новость проекта;
174-
- список новостей проекта исключает служебные feed-записи с `text = ""`;
175-
- новости проекта можно отметить просмотренными и лайкнуть;
176-
- пользователь может создавать новости только в своем профиле;
177-
- менеджер программы может создавать новости программы;
178-
- пользователь без роли менеджера не может создавать новости программы;
179-
- закрепленные новости программы идут выше обычных;
180-
- `/feed/?type=news` возвращает новости пользователя;
181-
- `/feed/?type=project` возвращает проектные новости как `type_model = "news"`;
182-
- feed исключает новости непубличных проектов.
155+
- manager, service и query helpers;
156+
- project/user/program API;
157+
- права на создание и изменение новостей;
158+
- лайки и просмотры;
159+
- исключение служебных feed-записей из списка новостей проекта;
160+
- сортировку закрепленных новостей программы.

docs/modules/projects.md

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ Projects отвечают за проектную часть Procollab: созд
4242
## Архитектура
4343

4444
- `projects/models.py` - модели проекта, участников, целей, компаний,
45-
ресурсов, ссылок, достижений и остаточной старой модели `ProjectNews`.
45+
ресурсов, ссылок и достижений.
4646
- `projects/views.py` - HTTP endpoints и значительная часть orchestration
4747
logic.
4848
- `projects/serializers.py` - request/response contracts, часть validation и
@@ -69,8 +69,6 @@ Projects отвечают за проектную часть Procollab: созд
6969
- `Resource` - ресурс проекта.
7070
- проектные новости - новости внутри проекта; актуальный API реализован через
7171
`news.News` с привязкой к `Project` через `content_type/object_id`.
72-
- `ProjectNews` - старая модель проектных новостей, оставшаяся после переноса
73-
данных в `news.News`.
7472
- `DefaultProjectCover` - дефолтная обложка проекта.
7573
- `DefaultProjectAvatar` - дефолтный аватар проекта.
7674

@@ -208,9 +206,6 @@ Projects отвечают за проектную часть Procollab: созд
208206
`news`: запись хранится в `news.News`, а связь с проектом задается через
209207
`content_type = Project` и `object_id = project.id`.
210208

211-
Старые `ProjectNews`, `ProjectNews*Serializer` и `ProjectNews*View` остаются в
212-
коде, но текущие routes проекта подключены к `news.views`.
213-
214209
## Ограничения и правила
215210

216211
- Публичный каталог показывает только `draft = False` и `is_public = True`.
@@ -222,7 +217,7 @@ Projects отвечают за проектную часть Procollab: созд
222217
- Компания может быть связана с проектом только один раз.
223218
- Ресурс может ссылаться только на компанию, уже привязанную к проекту.
224219
- Проектные новости являются живым frontend-сценарием, но текущие routes
225-
используют общий модуль `news`, а не старую модель `ProjectNews`.
220+
используют общий модуль `news`.
226221

227222
## Тесты
228223

feed/serializers.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
from files.serializers import UserFileSerializer
66
from news.mapping import NewsMapping
77
from news.models import News
8+
from news.services import is_content_news
89
from projects.models import Project
910
from users.models import CustomUser
1011

1112

12-
class NewsFeedListSerializer(serializers.ModelSerializer):
13+
class FeedNewsResponseSerializer(serializers.ModelSerializer):
1314
name = serializers.SerializerMethodField()
1415
image_address = serializers.SerializerMethodField()
1516
is_user_liked = serializers.SerializerMethodField()
@@ -21,13 +22,13 @@ class NewsFeedListSerializer(serializers.ModelSerializer):
2122

2223
def get_type_model(self, obj) -> str:
2324
model_type = CONTENT_OBJECT_MAPPING[obj.content_type.model]
24-
if obj.text != "" and model_type == "project":
25+
if is_content_news(obj) and model_type == "project":
2526
return "news"
2627
return model_type
2728

2829
def get_content_object(self, obj) -> dict:
2930
type_model = obj.content_type.model
30-
if obj.text != "" and self.get_type_model(obj) == "project":
31+
if is_content_news(obj) and self.get_type_model(obj) == "project":
3132
type_model = "news"
3233
serializer = CONTENT_OBJECT_SERIALIZER_MAPPING[type_model](obj.content_object)
3334
return serializer.data
@@ -41,7 +42,7 @@ def get_likes_count(self, obj):
4142
def get_name(self, obj):
4243
if obj.content_type.model == CustomUser.__name__.lower():
4344
return f"{obj.content_object.first_name} {obj.content_object.last_name}"
44-
elif obj.text != "" and obj.content_type.model == Project.__name__.lower():
45+
elif is_content_news(obj) and obj.content_type.model == Project.__name__.lower():
4546
return f"{obj.content_object.name}"
4647

4748
def get_image_address(self, obj):

0 commit comments

Comments
 (0)