Skip to content

Commit 9102c85

Browse files
authored
Merge pull request #659 from PROCOLLAB-github/feature/application-team-model
Add application team models
2 parents 14a563e + f63beda commit 9102c85

6 files changed

Lines changed: 971 additions & 38 deletions

File tree

docs/application-team-model.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Application Team Model
2+
3+
## Назначение
4+
5+
Базовый слой командных заявок разделяет три понятия:
6+
7+
- `Application` — заявка на одну партнерскую программу;
8+
- `Team` — команда, собранная только для этой заявки;
9+
- `TeamMember` — членство пользователя в команде заявки.
10+
11+
Публичного Team API и атомарного service создания команды пока нет. Модели
12+
подготавливают структуру данных для следующих PR, не меняя текущий
13+
индивидуальный API flow.
14+
15+
## Participation mode
16+
17+
В `Application` добавлено поле `participation_mode`:
18+
19+
| Значение | Смысл |
20+
|---|---|
21+
| `undecided` | Формат участия еще не выбран |
22+
| `individual` | Индивидуальная заявка |
23+
| `team` | Командная заявка |
24+
25+
Runtime default временно равен `individual`: существующие API и frontend не
26+
передают новое поле, поэтому все текущие и исторические Application продолжают
27+
работать как индивидуальные. `undecided` должен стать default одновременно с
28+
wizard и запретом submit заявки без выбранного формата.
29+
30+
Миграция `0020_team_application_participation_mode_teammember_and_more`
31+
добавляет поле с default `individual`; Django применяет это значение ко всем
32+
существующим строкам Application.
33+
34+
## Team
35+
36+
`Team` содержит:
37+
38+
- one-to-one `application` с `related_name="team"`;
39+
- необязательное на стадии черновика `name` длиной до 255 символов;
40+
- `captain`;
41+
- `created_at` и `updated_at`.
42+
43+
Team относится к Application, потому что состав команды может различаться в
44+
разных активностях даже при использовании одного Project. Существующий
45+
`projects.Collaborator` описывает постоянного участника Project и намеренно не
46+
переиспользуется как TeamMember.
47+
48+
В первом MVP `Team.captain` обязан совпадать с `Application.user`.
49+
`Team.status`, invite code, token и настройки размера команды не добавлены:
50+
редактируемость будущего Team flow должна выводиться из `Application.status`.
51+
52+
## TeamMember
53+
54+
`TeamMember` содержит:
55+
56+
- `team` и `user`;
57+
- роль `captain` или `member`;
58+
- статус `invited`, `accepted`, `declined`, `removed` или `left`;
59+
- nullable `invited_by`;
60+
- nullable `joined_at`;
61+
- `created_at` и `updated_at`.
62+
63+
Для первоначального captain member `invited_by` остается `null`: капитан не
64+
принимает собственное приглашение, а создается как владелец команды. Статус
65+
`invited` для обычных участников пока является только модельной заготовкой;
66+
механизм TeamInvite в этом PR отсутствует.
67+
68+
При первом сохранении статуса `accepted` модель автоматически заполняет
69+
`joined_at`, если дата не передана. При переходе в `removed` или `left` дата не
70+
очищается и сохраняет момент фактического присоединения.
71+
72+
## Database constraints
73+
74+
На уровне БД обеспечены:
75+
76+
- одна Team на Application через `OneToOneField`;
77+
- уникальность `TeamMember(team, user)`;
78+
- не более одного `accepted` TeamMember с ролью `captain` в Team;
79+
- check: роль `captain` допустима только со статусом `accepted`.
80+
81+
Простым constraint одной таблицы нельзя надежно обеспечить:
82+
83+
- участие пользователя в нескольких Team разных Application одной Program;
84+
- конфликт TeamMember с индивидуальной Application той же Program;
85+
- Registration всех членов команды;
86+
- размер команды;
87+
- блокировку состава после submit.
88+
89+
Эти правила проходят через Application, Team и TeamMember, поэтому требуют
90+
отдельного транзакционного domain service с блокировкой строк.
91+
92+
## Model validation
93+
94+
`Team.clean()` проверяет:
95+
96+
- `Application.participation_mode == team`;
97+
- `Team.captain == Application.user`.
98+
99+
Это запрещает Team для `individual` и `undecided` Application.
100+
101+
`TeamMember.clean()` проверяет:
102+
103+
- captain member имеет статус `accepted`;
104+
- его `user` совпадает с `Team.captain`;
105+
- для первого принятия заполнен `joined_at`.
106+
107+
Наличие captain TeamMember намеренно не проверяется при первом `Team.save()`.
108+
Такая проверка создала бы цикл: Team должна быть сохранена до создания
109+
TeamMember, а TeamMember уже требует сохраненную Team.
110+
111+
## Текущий технический порядок создания
112+
113+
До появления публичного API техническая последовательность выглядит так:
114+
115+
1. создать или обновить Application с `participation_mode=team`;
116+
2. создать Team с captain, совпадающим с `Application.user`;
117+
3. создать accepted TeamMember с `role=captain` и тем же пользователем.
118+
119+
Клиентам нельзя использовать эту последовательность напрямую. Сейчас нет
120+
публичных serializers/views/URLs и транзакционного service, который откатит
121+
частично созданную команду.
122+
123+
## Вне текущего MVP
124+
125+
Следующие PR должны добавить:
126+
127+
- атомарный creation/invariant service;
128+
- проверку Registration и одной активной заявки на Program;
129+
- Team permissions и публичный Team API;
130+
- блокировку состава по Application status;
131+
- TeamInvite, accept/decline/revoke/expire;
132+
- email и внутренние уведомления;
133+
- program policy по формату и размеру команды;
134+
- frontend wizard и вкладку команды.
135+
136+
Project, `projects.Collaborator`, legacy `invites.Invite`, Submission и
137+
существующие Application endpoints этим слоем данных не изменяются.

0 commit comments

Comments
 (0)