Skip to content

Commit fa298f0

Browse files
authored
Merge pull request #637 from PROCOLLAB-github/refactor/modules
Refactor/modules
2 parents 9feef3d + cbc485b commit fa298f0

29 files changed

Lines changed: 884 additions & 618 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: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,160 @@
11
# News
22

3-
TODO
3+
## Назначение
4+
5+
News отвечает за публикации и новости в разных частях продукта.
6+
7+
Модуль хранит новости в единой модели `News` и связывает их с объектами через
8+
generic relation:
9+
10+
- проектами;
11+
- пользователями;
12+
- партнерскими программами;
13+
- объектами ленты, например вакансиями.
14+
15+
Одна и та же модель используется для двух сценариев:
16+
17+
- обычная новость с текстом и файлами;
18+
- служебная запись ленты для существующего объекта, где `text = ""`.
19+
20+
## Статус модуля
21+
22+
Модуль рабочий, но связан с несколькими доменными областями и требует
23+
аккуратного рефакторинга. Сейчас он обслуживает проектные новости, новости
24+
пользователей, новости программ и часть общей ленты.
25+
26+
Первый слой regression-тестов добавлен для живых сценариев API.
27+
28+
## Основные возможности
29+
30+
- создание новости в контексте проекта;
31+
- создание новости в профиле пользователя;
32+
- создание новости в партнерской программе;
33+
- просмотр списка новостей в конкретном контексте;
34+
- просмотр, редактирование и удаление отдельной новости;
35+
- отметка просмотра новости;
36+
- лайк и снятие лайка с новости;
37+
- участие новостей в общей ленте `/feed/`;
38+
- закрепление новости программы через `pin`.
39+
40+
## Архитектура
41+
42+
- `news/models.py` - модель `News` с `content_type/object_id`, файлами, лайками,
43+
просмотрами и флагом `pin`.
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.
50+
- `news/views.py` - общий API для list/create/detail/update/delete, set_viewed и
51+
set_liked.
52+
- `news/serializers.py` - request/response serializers для создания, списка и
53+
detail.
54+
- `news/permissions.py` - права на создание и изменение новости в зависимости от
55+
связанного объекта.
56+
- `news/admin.py` - админка `News`.
57+
- `news/tests/` - regression-тесты живых сценариев модуля.
58+
59+
## Основные сущности
60+
61+
- `News` - публикация или запись ленты.
62+
- `content_object` - объект, к которому относится новость.
63+
- `files` - вложения новости.
64+
- `likes` - generic likes через `core.Like`.
65+
- `views` - generic views через `core.View`.
66+
- `pin` - закрепление новости, сейчас используется для новостей программ.
67+
68+
Feed-запись определяется через helper `is_feed_record(news)`, а обычная новость
69+
через `is_content_news(news)`. Сейчас оба helper'а используют текущий признак
70+
`text`, но вызывающий код не должен напрямую проверять `text == ""`.
71+
72+
## API
73+
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:
91+
92+
- `GET /news/` - подключен напрямую, но без контекста возвращает пустой список.
93+
- `GET /news/<news_id>/` - подключен напрямую, но без контекста не является
94+
основным пользовательским сценарием.
95+
- `GET /feed/?type=...` - общая лента, которая читает данные из `News`.
96+
97+
## Основные сценарии
98+
99+
### 1. Новость проекта
100+
101+
Лидер проекта создает новость через `/projects/<id>/news/`.
102+
103+
Новость сохраняется в `news.News`, а связь с проектом задается через
104+
`content_type = Project` и `object_id = project.id`.
105+
106+
Новости проекта с текстом отображаются внутри проекта. Служебные feed-записи
107+
из списка проектных новостей исключаются.
108+
109+
### 2. Новость пользователя
110+
111+
Пользователь создает новость в своем профиле через `/auth/users/<id>/news/`.
112+
Создавать новость за другого пользователя нельзя.
113+
114+
### 3. Новость программы
115+
116+
Менеджер партнерской программы создает новость через `/programs/<id>/news/`.
117+
Пользователь без роли менеджера программы не может создавать и изменять такие
118+
новости.
119+
120+
Новости программ сортируются с учетом `pin`: закрепленные новости идут выше
121+
обычных.
122+
123+
### 4. Просмотры и лайки
124+
125+
Просмотры и лайки работают через generic-модели `core.View` и `core.Like`.
126+
Они привязаны к самой новости, а не к объекту, которому эта новость посвящена.
127+
128+
### 5. Лента
129+
130+
`/feed/` читает `News` и возвращает элементы в формате, зависящем от типа
131+
связанного объекта.
132+
133+
Для проектных записей важно различать:
134+
135+
- `text = ""` - служебная запись ленты о проекте;
136+
- новость с текстом - полноценная новость проекта, которая в ленте возвращается
137+
как `type_model = "news"`.
138+
139+
Лента исключает новости, связанные с непубличными или черновыми проектами.
140+
141+
## Ограничения и правила
142+
143+
- Новость проекта может создавать и изменять только лидер проекта.
144+
- Новость пользователя может создавать и изменять только сам пользователь.
145+
- Новость программы может создавать и изменять только менеджер программы.
146+
- Прямой `/news/` без project/user/program context не является основным
147+
пользовательским API.
148+
- Несуществующий project/user/program context возвращает `404`.
149+
- Проектные новости реализованы через `news.News`.
150+
151+
## Тесты
152+
153+
Текущие regression-тесты проверяют:
154+
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):

feed/services.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from core.models import Like
44
from feed.constants import SIGNALS_MODELS
55
from news.models import News
6+
from news.services import FEED_RECORD_TEXT
67
from users.models import CustomUser
78

89

@@ -18,12 +19,18 @@ def get_liked_news(user: CustomUser, queryset: list[News]) -> list[int]:
1819
def delete_news_for_model(instance: SIGNALS_MODELS):
1920
content_type = ContentType.objects.get_for_model(instance)
2021
obj = News.objects.filter(
21-
text="", content_type=content_type, object_id=instance.id
22+
text=FEED_RECORD_TEXT,
23+
content_type=content_type,
24+
object_id=instance.id,
2225
).first()
2326
if obj:
2427
obj.delete()
2528

2629

2730
def create_news_for_model(instance: SIGNALS_MODELS):
2831
content_type = ContentType.objects.get_for_model(instance)
29-
News.objects.get_or_create(text="", content_type=content_type, object_id=instance.id)
32+
News.objects.get_or_create(
33+
text=FEED_RECORD_TEXT,
34+
content_type=content_type,
35+
object_id=instance.id,
36+
)

feed/tests/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)