Skip to content

Commit dd1cd4e

Browse files
authored
Merge pull request #603 from PROCOLLAB-github/feature/auto_sending_email
Feature/auto sending email
2 parents 7c89b73 + f62c4f3 commit dd1cd4e

15 files changed

+782
-74
lines changed

core/admin.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515

1616
class SkillToObjectInline(GenericStackedInline):
1717
model = SkillToObject
18-
extra = 1
18+
extra = 0
19+
autocomplete_fields = ("skill",)
1920
verbose_name = "Навык"
2021
verbose_name_plural = "Навыки"
2122

@@ -49,6 +50,10 @@ class SkillAdmin(admin.ModelAdmin):
4950
"id",
5051
"name",
5152
)
53+
search_fields = (
54+
"name",
55+
"category__name",
56+
)
5257

5358

5459
@admin.register(SkillCategory)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Generated by Django 4.2.11 on 2026-02-09 09:39
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("mailing", "0008_mailing_scenario_log"),
10+
]
11+
12+
operations = [
13+
migrations.RenameIndex(
14+
model_name="mailingscenariolog",
15+
new_name="mailing_mai_scenari_eed98a_idx",
16+
old_name="mailing_ma_scenari_73b1f9_idx",
17+
),
18+
migrations.RenameIndex(
19+
model_name="mailingscenariolog",
20+
new_name="mailing_mai_program_63bc97_idx",
21+
old_name="mailing_ma_program_b9dcf9_idx",
22+
),
23+
migrations.RenameIndex(
24+
model_name="mailingscenariolog",
25+
new_name="mailing_mai_user_id_333e66_idx",
26+
old_name="mailing_ma_user_id_0e2a92_idx",
27+
),
28+
]

mailing/rendering.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from partner_programs.models import PartnerProgram
2+
from users.models import CustomUser
3+
4+
5+
def render_subject(subject: str, program: PartnerProgram) -> str:
6+
return subject.replace("{program_name}", program.name)
7+
8+
9+
def render_template_value(
10+
value: str,
11+
program: PartnerProgram,
12+
user: CustomUser,
13+
) -> str:
14+
return (
15+
value.replace("{program_name}", program.name)
16+
.replace("{program_id}", str(program.id))
17+
.replace("{user_id}", str(user.id))
18+
)

mailing/scenarios.py

Lines changed: 123 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from enum import Enum
44
from typing import Callable
55

6+
from mailing.rendering import render_template_value
67
from partner_programs.models import PartnerProgram
78
from users.models import CustomUser
89

@@ -12,13 +13,18 @@
1213
class TriggerType(Enum):
1314
PROGRAM_SUBMISSION_DEADLINE = "program_submission_deadline"
1415
PROGRAM_REGISTRATION_DATE = "program_registration_date"
16+
PROGRAM_REGISTRATION_END = "program_registration_end"
1517

1618

1719
class RecipientRule(Enum):
1820
ALL_PARTICIPANTS = "all_participants"
1921
NO_PROJECT_IN_PROGRAM = "no_project_in_program"
2022
NO_PROJECT_IN_PROGRAM_REGISTERED_ON_DATE = "no_project_in_program_registered_on_date"
2123
PROJECT_NOT_SUBMITTED = "project_not_submitted"
24+
INACTIVE_ACCOUNT_IN_PROGRAM = "inactive_account_in_program"
25+
INACTIVE_ACCOUNT_IN_PROGRAM_REGISTERED_ON_DATE = (
26+
"inactive_account_in_program_registered_on_date"
27+
)
2228

2329

2430
ContextBuilder = Callable[[PartnerProgram, CustomUser, date], dict]
@@ -35,46 +41,25 @@ class Scenario:
3541
context_builder: ContextBuilder
3642

3743

38-
def _build_submission_deadline_context(offset_days: int) -> ContextBuilder:
39-
def _builder(program: PartnerProgram, user: CustomUser, deadline_date: date) -> dict:
40-
return {
41-
"preview_text": "Кейс-чемпионат уже стартовал",
42-
"title": "Время начинать!",
43-
"text": (
44-
"Кейс-чемпионат уже стартовал. Скорее заходите на платформу, "
45-
"создавайте проект и подключайте команду к работе.\n\n"
46-
"Вас ждет много интересного ⚡"
47-
),
48-
"button_text": "Подать проект",
49-
"button_link": f"{FRONTEND_BASE_URL}/office/program/{program.id}",
50-
}
51-
52-
return _builder
53-
54-
55-
def _build_registration_plus_5_context() -> ContextBuilder:
44+
def _build_context(
45+
*,
46+
preview_text: str,
47+
title: str,
48+
text: str,
49+
button_text: str | None = None,
50+
button_link: str | None = None,
51+
) -> ContextBuilder:
5652
def _builder(program: PartnerProgram, user: CustomUser, _ref_date: date) -> dict:
57-
return {
58-
"preview_text": "Сделайте первый шаг в программе",
59-
"title": "Сделать первый шаг",
60-
"text": (
61-
"Когда непонятно с чего начать — стоит начать с самого простого. "
62-
"На раз-два-три: зайти на платформу — создать проект — "
63-
"пригласить команду.\n\n"
64-
"И вот, первый шаг уже сделан"
65-
),
66-
}
67-
68-
return _builder
69-
70-
71-
def _build_project_not_submitted_context(title: str, text: str) -> ContextBuilder:
72-
def _builder(program: PartnerProgram, user: CustomUser, _ref_date: date) -> dict:
73-
return {
74-
"preview_text": title,
75-
"title": title,
76-
"text": text,
53+
context = {
54+
"preview_text": render_template_value(preview_text, program, user),
55+
"title": render_template_value(title, program, user),
56+
"text": render_template_value(text, program, user),
7757
}
58+
if button_text is not None:
59+
context["button_text"] = render_template_value(button_text, program, user)
60+
if button_link is not None:
61+
context["button_link"] = render_template_value(button_link, program, user)
62+
return context
7863

7964
return _builder
8065

@@ -85,61 +70,137 @@ def _builder(program: PartnerProgram, user: CustomUser, _ref_date: date) -> dict
8570
trigger=TriggerType.PROGRAM_SUBMISSION_DEADLINE,
8671
offset_days=10,
8772
template_name="email/generic-template-0.html",
88-
subject="Время начинать!",
73+
subject="{program_name}: важное сообщение",
8974
recipient_rule=RecipientRule.NO_PROJECT_IN_PROGRAM,
90-
context_builder=_build_submission_deadline_context(10),
75+
context_builder=_build_context(
76+
preview_text="Кейс-чемпионат уже стартовал",
77+
title="Время начинать!",
78+
text=(
79+
"Кейс-чемпионат уже стартовал. Скорее заходите на платформу, "
80+
"создавайте проект и подключайте команду к работе.\n\n"
81+
"Вас ждет много интересного ⚡"
82+
),
83+
button_text="Создать проект",
84+
button_link=f"{FRONTEND_BASE_URL}/office/projects",
85+
),
9186
),
9287
Scenario(
9388
code="program_registration_plus_5_no_project",
9489
trigger=TriggerType.PROGRAM_REGISTRATION_DATE,
9590
offset_days=5,
9691
template_name="email/generic-template-0.html",
97-
subject="Сделать первый шаг",
92+
subject="{program_name}: важное сообщение",
9893
recipient_rule=RecipientRule.NO_PROJECT_IN_PROGRAM_REGISTERED_ON_DATE,
99-
context_builder=_build_registration_plus_5_context(),
94+
context_builder=_build_context(
95+
preview_text="Сделать первый шаг",
96+
title="Сделать первый шаг",
97+
text=(
98+
"Когда непонятно с чего начать — стоит начать с самого простого. "
99+
"Например, зайти на платформу, создать проект или вступить в уже "
100+
"созданный лидером вашей команды.\n\n"
101+
"И вот, первый шаг уже сделан!"
102+
),
103+
button_text="Зайти на платформу",
104+
button_link=f"{FRONTEND_BASE_URL}/office/projects",
105+
),
106+
),
107+
Scenario(
108+
code="program_registration_plus_3_inactive_account",
109+
trigger=TriggerType.PROGRAM_REGISTRATION_DATE,
110+
offset_days=3,
111+
template_name="email/generic-template-0.html",
112+
subject="{program_name}: важное сообщение",
113+
recipient_rule=RecipientRule.INACTIVE_ACCOUNT_IN_PROGRAM_REGISTERED_ON_DATE,
114+
context_builder=_build_context(
115+
preview_text="Поздравляем!",
116+
title="Поздравляем!",
117+
text=(
118+
"Вы зарегистрировались на {program_name}. "
119+
"Заходите на платформу, чтобы оформить свой профиль участника "
120+
"и вступить в закрытую группу программы.\n\n"
121+
"Увидимся на платформе ⚡"
122+
),
123+
button_text="Оформить профиль",
124+
button_link=f"{FRONTEND_BASE_URL}/office/profile/{{user_id}}/",
125+
),
126+
),
127+
Scenario(
128+
code="program_registration_end_plus_3_inactive_account",
129+
trigger=TriggerType.PROGRAM_REGISTRATION_END,
130+
offset_days=3,
131+
template_name="email/generic-template-0.html",
132+
subject="{program_name}: важное сообщение",
133+
recipient_rule=RecipientRule.INACTIVE_ACCOUNT_IN_PROGRAM,
134+
context_builder=_build_context(
135+
preview_text="Без вас совсем не то",
136+
title="Без вас совсем не то",
137+
text=(
138+
"Мы так обрадовались, увидев вашу регистрацию, но, кажется, "
139+
"вы еще не заходили на платформу.\n\n"
140+
"Скорее заходите на procollab, чтобы стать активным участником "
141+
"программы и забрать максимум полезного для себя ⚡"
142+
),
143+
button_text="Зайти на платформу",
144+
button_link=f"{FRONTEND_BASE_URL}/office/profile/{{user_id}}/",
145+
),
100146
),
101147
Scenario(
102148
code="program_submission_deadline_minus_9_project_not_submitted",
103149
trigger=TriggerType.PROGRAM_SUBMISSION_DEADLINE,
104150
offset_days=9,
105151
template_name="email/generic-template-0.html",
106-
subject="Кейс-задания опубликованы",
152+
subject="{program_name}: важное сообщение",
107153
recipient_rule=RecipientRule.PROJECT_NOT_SUBMITTED,
108-
context_builder=_build_project_not_submitted_context(
109-
"Кейс-задания опубликованы",
110-
"Заходите на платформу, чтобы познакомиться с кейсами первого этапа "
111-
"кейс-чемпионата. Кейсы загружены в материалы закрытой группы.\n\n"
112-
"Приступайте к работе уже сегодня, чтобы успеть подготовить итоговое "
113-
"решение в срок ⚡",
154+
context_builder=_build_context(
155+
preview_text="Кейс-задания опубликованы",
156+
title="Кейс-задания опубликованы",
157+
text=(
158+
"Заходите на платформу, чтобы познакомиться с кейсами первого этапа "
159+
"кейс-чемпионата. Кейсы загружены в материалы закрытой группы.\n\n"
160+
"Приступайте к работе уже сегодня, чтобы успеть подготовить итоговое "
161+
"решение в срок ⚡"
162+
),
163+
button_text="Познакомиться с кейсом",
164+
button_link=f"{FRONTEND_BASE_URL}/office/program/{{program_id}}",
114165
),
115166
),
116167
Scenario(
117168
code="program_submission_deadline_minus_3_project_not_submitted",
118169
trigger=TriggerType.PROGRAM_SUBMISSION_DEADLINE,
119170
offset_days=3,
120171
template_name="email/generic-template-0.html",
121-
subject="До сдачи итогового решения осталось 3 дня",
172+
subject="{program_name}: важное сообщение",
122173
recipient_rule=RecipientRule.PROJECT_NOT_SUBMITTED,
123-
context_builder=_build_project_not_submitted_context(
124-
"До сдачи итогового решения осталось 3 дня",
125-
"Работа в самом разгаре, и мы запускаем обратный отсчет. "
126-
"Осталось всего 3 дня, чтобы доработать проект, оформить презентацию "
127-
"и загрузить итоговое решение на платформу.",
174+
context_builder=_build_context(
175+
preview_text="До сдачи итогового решения осталось 3 дня",
176+
title="До сдачи итогового решения осталось 3 дня",
177+
text=(
178+
"Работа в самом разгаре, и мы запускаем обратный отсчет. "
179+
"Осталось всего 3 дня, чтобы доработать проект, оформить презентацию "
180+
"и загрузить итоговое решение на платформу."
181+
),
182+
button_text="Загрузить решение",
183+
button_link=f"{FRONTEND_BASE_URL}/office/projects",
128184
),
129185
),
130186
Scenario(
131187
code="program_submission_deadline_minus_1_project_not_submitted",
132188
trigger=TriggerType.PROGRAM_SUBMISSION_DEADLINE,
133189
offset_days=1,
134190
template_name="email/generic-template-0.html",
135-
subject="1 день до сдачи итогового решения",
191+
subject="{program_name}: важное сообщение",
136192
recipient_rule=RecipientRule.PROJECT_NOT_SUBMITTED,
137-
context_builder=_build_project_not_submitted_context(
138-
"1 день до сдачи итогового решения",
139-
"День X совсем скоро. Осталось только внести последние штрихи и "
140-
"загрузить итоговое решение на платформу.\n\n"
141-
"По любым техническим вопросам всегда на связи @procollab_support\n\n"
142-
"Удачи!",
193+
context_builder=_build_context(
194+
preview_text="1 день до сдачи итогового решения",
195+
title="1 день до сдачи итогового решения",
196+
text=(
197+
"День X совсем скоро. Осталось только внести последние штрихи и "
198+
"загрузить итоговое решение на платформу.\n\n"
199+
"По любым техническим вопросам всегда на связи @procollab_support\n\n"
200+
"Удачи!"
201+
),
202+
button_text="Загрузить решение",
203+
button_link=f"{FRONTEND_BASE_URL}/office/program/{{program_id}}",
143204
),
144205
),
145206
)

mailing/tasks.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@
55

66
from mailing.constants import FAILED_ANYMAIL_STATUSES
77
from mailing.models import MailingScenarioLog
8+
from mailing.rendering import render_subject
89
from mailing.scenarios import RecipientRule, SCENARIOS, TriggerType
910
from mailing.utils import send_mass_mail_from_template
1011
from partner_programs.selectors import (
1112
program_participants,
13+
program_participants_with_inactive_account,
14+
program_participants_with_inactive_account_registered_on,
1215
program_participants_with_unsubmitted_project,
1316
program_participants_without_project_registered_on,
1417
program_participants_without_project,
18+
programs_with_registration_end_on,
1519
programs_with_registrations_on,
1620
programs_with_submission_deadline_on,
1721
)
@@ -26,22 +30,34 @@ def _get_programs_for_scenario(scenario, target_date):
2630
return programs_with_submission_deadline_on(target_date)
2731
case TriggerType.PROGRAM_REGISTRATION_DATE:
2832
return programs_with_registrations_on(target_date)
33+
case TriggerType.PROGRAM_REGISTRATION_END:
34+
return programs_with_registration_end_on(target_date)
2935
case _:
3036
raise ValueError(f"Unsupported trigger: {scenario.trigger}")
3137

3238

33-
def _get_recipients(scenario, program_id: int, target_date):
39+
def _get_recipients(scenario, program, target_date):
3440
match scenario.recipient_rule:
3541
case RecipientRule.ALL_PARTICIPANTS:
36-
return program_participants(program_id)
42+
return program_participants(program.id)
3743
case RecipientRule.NO_PROJECT_IN_PROGRAM:
38-
return program_participants_without_project(program_id)
44+
return program_participants_without_project(program.id)
3945
case RecipientRule.NO_PROJECT_IN_PROGRAM_REGISTERED_ON_DATE:
4046
return program_participants_without_project_registered_on(
41-
program_id, target_date
47+
program.id, target_date
4248
)
4349
case RecipientRule.PROJECT_NOT_SUBMITTED:
44-
return program_participants_with_unsubmitted_project(program_id)
50+
return program_participants_with_unsubmitted_project(program.id)
51+
case RecipientRule.INACTIVE_ACCOUNT_IN_PROGRAM:
52+
return program_participants_with_inactive_account(
53+
program.id, program.datetime_started
54+
)
55+
case RecipientRule.INACTIVE_ACCOUNT_IN_PROGRAM_REGISTERED_ON_DATE:
56+
return program_participants_with_inactive_account_registered_on(
57+
program.id,
58+
target_date,
59+
program.datetime_started,
60+
)
4561
case _:
4662
raise ValueError(f"Unsupported recipient rule: {scenario.recipient_rule}")
4763

@@ -52,7 +68,7 @@ def _deadline_date(program):
5268

5369

5470
def _send_scenario_for_program(scenario, program, scheduled_for, target_date):
55-
recipients = _get_recipients(scenario, program.id, target_date)
71+
recipients = _get_recipients(scenario, program, target_date)
5672
if not recipients.exists():
5773
return 0
5874

@@ -197,7 +213,7 @@ def status_callback(user, msg):
197213
try:
198214
num_sent = send_mass_mail_from_template(
199215
recipients_to_send,
200-
scenario.subject,
216+
render_subject(scenario.subject, program),
201217
scenario.template_name,
202218
context_builder=context_builder,
203219
status_callback=status_callback,

0 commit comments

Comments
 (0)