Skip to content

Commit 7c89b73

Browse files
authored
Merge pull request #602 from PROCOLLAB-github/feature/auto_sending_email
Добавлены новые сценарии рассылок
2 parents 3b57c50 + a2fefc3 commit 7c89b73

5 files changed

Lines changed: 167 additions & 17 deletions

File tree

mailing/scenarios.py

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@
1111

1212
class TriggerType(Enum):
1313
PROGRAM_SUBMISSION_DEADLINE = "program_submission_deadline"
14+
PROGRAM_REGISTRATION_DATE = "program_registration_date"
1415

1516

1617
class RecipientRule(Enum):
1718
ALL_PARTICIPANTS = "all_participants"
1819
NO_PROJECT_IN_PROGRAM = "no_project_in_program"
20+
NO_PROJECT_IN_PROGRAM_REGISTERED_ON_DATE = "no_project_in_program_registered_on_date"
21+
PROJECT_NOT_SUBMITTED = "project_not_submitted"
1922

2023

2124
ContextBuilder = Callable[[PartnerProgram, CustomUser, date], dict]
@@ -34,14 +37,13 @@ class Scenario:
3437

3538
def _build_submission_deadline_context(offset_days: int) -> ContextBuilder:
3639
def _builder(program: PartnerProgram, user: CustomUser, deadline_date: date) -> dict:
37-
deadline_str = deadline_date.strftime("%d.%m.%Y")
3840
return {
39-
"preview_text": f"До окончания подачи проектов осталось {offset_days} дней",
40-
"title": "Пора подать проект",
41+
"preview_text": "Кейс-чемпионат уже стартовал",
42+
"title": "Время начинать!",
4143
"text": (
42-
f"До окончания подачи проектов в программе «{program.name}» "
43-
f"осталось {offset_days} дней. "
44-
f"Пожалуйста, подайте проект и сформируйте команду до {deadline_str}."
44+
"Кейс-чемпионат уже стартовал. Скорее заходите на платформу, "
45+
"создавайте проект и подключайте команду к работе.\n\n"
46+
"Вас ждет много интересного ⚡"
4547
),
4648
"button_text": "Подать проект",
4749
"button_link": f"{FRONTEND_BASE_URL}/office/program/{program.id}",
@@ -50,14 +52,94 @@ def _builder(program: PartnerProgram, user: CustomUser, deadline_date: date) ->
5052
return _builder
5153

5254

55+
def _build_registration_plus_5_context() -> ContextBuilder:
56+
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,
77+
}
78+
79+
return _builder
80+
81+
5382
SCENARIOS: tuple[Scenario, ...] = (
5483
Scenario(
5584
code="program_submission_deadline_minus_10_no_project",
5685
trigger=TriggerType.PROGRAM_SUBMISSION_DEADLINE,
5786
offset_days=10,
5887
template_name="email/generic-template-0.html",
59-
subject="Procollab | Подача проекта",
88+
subject="Время начинать!",
6089
recipient_rule=RecipientRule.NO_PROJECT_IN_PROGRAM,
6190
context_builder=_build_submission_deadline_context(10),
6291
),
92+
Scenario(
93+
code="program_registration_plus_5_no_project",
94+
trigger=TriggerType.PROGRAM_REGISTRATION_DATE,
95+
offset_days=5,
96+
template_name="email/generic-template-0.html",
97+
subject="Сделать первый шаг",
98+
recipient_rule=RecipientRule.NO_PROJECT_IN_PROGRAM_REGISTERED_ON_DATE,
99+
context_builder=_build_registration_plus_5_context(),
100+
),
101+
Scenario(
102+
code="program_submission_deadline_minus_9_project_not_submitted",
103+
trigger=TriggerType.PROGRAM_SUBMISSION_DEADLINE,
104+
offset_days=9,
105+
template_name="email/generic-template-0.html",
106+
subject="Кейс-задания опубликованы",
107+
recipient_rule=RecipientRule.PROJECT_NOT_SUBMITTED,
108+
context_builder=_build_project_not_submitted_context(
109+
"Кейс-задания опубликованы",
110+
"Заходите на платформу, чтобы познакомиться с кейсами первого этапа "
111+
"кейс-чемпионата. Кейсы загружены в материалы закрытой группы.\n\n"
112+
"Приступайте к работе уже сегодня, чтобы успеть подготовить итоговое "
113+
"решение в срок ⚡",
114+
),
115+
),
116+
Scenario(
117+
code="program_submission_deadline_minus_3_project_not_submitted",
118+
trigger=TriggerType.PROGRAM_SUBMISSION_DEADLINE,
119+
offset_days=3,
120+
template_name="email/generic-template-0.html",
121+
subject="До сдачи итогового решения осталось 3 дня",
122+
recipient_rule=RecipientRule.PROJECT_NOT_SUBMITTED,
123+
context_builder=_build_project_not_submitted_context(
124+
"До сдачи итогового решения осталось 3 дня",
125+
"Работа в самом разгаре, и мы запускаем обратный отсчет. "
126+
"Осталось всего 3 дня, чтобы доработать проект, оформить презентацию "
127+
"и загрузить итоговое решение на платформу.",
128+
),
129+
),
130+
Scenario(
131+
code="program_submission_deadline_minus_1_project_not_submitted",
132+
trigger=TriggerType.PROGRAM_SUBMISSION_DEADLINE,
133+
offset_days=1,
134+
template_name="email/generic-template-0.html",
135+
subject="1 день до сдачи итогового решения",
136+
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+
"Удачи!",
143+
),
144+
),
63145
)

mailing/tasks.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
from mailing.utils import send_mass_mail_from_template
1010
from partner_programs.selectors import (
1111
program_participants,
12+
program_participants_with_unsubmitted_project,
13+
program_participants_without_project_registered_on,
1214
program_participants_without_project,
15+
programs_with_registrations_on,
1316
programs_with_submission_deadline_on,
1417
)
1518
from procollab.celery import app
@@ -21,16 +24,24 @@ def _get_programs_for_scenario(scenario, target_date):
2124
match scenario.trigger:
2225
case TriggerType.PROGRAM_SUBMISSION_DEADLINE:
2326
return programs_with_submission_deadline_on(target_date)
27+
case TriggerType.PROGRAM_REGISTRATION_DATE:
28+
return programs_with_registrations_on(target_date)
2429
case _:
2530
raise ValueError(f"Unsupported trigger: {scenario.trigger}")
2631

2732

28-
def _get_recipients(scenario, program_id: int):
33+
def _get_recipients(scenario, program_id: int, target_date):
2934
match scenario.recipient_rule:
3035
case RecipientRule.ALL_PARTICIPANTS:
3136
return program_participants(program_id)
3237
case RecipientRule.NO_PROJECT_IN_PROGRAM:
3338
return program_participants_without_project(program_id)
39+
case RecipientRule.NO_PROJECT_IN_PROGRAM_REGISTERED_ON_DATE:
40+
return program_participants_without_project_registered_on(
41+
program_id, target_date
42+
)
43+
case RecipientRule.PROJECT_NOT_SUBMITTED:
44+
return program_participants_with_unsubmitted_project(program_id)
3445
case _:
3546
raise ValueError(f"Unsupported recipient rule: {scenario.recipient_rule}")
3647

@@ -40,8 +51,8 @@ def _deadline_date(program):
4051
return timezone.localtime(deadline).date()
4152

4253

43-
def _send_scenario_for_program(scenario, program, scheduled_for):
44-
recipients = _get_recipients(scenario, program.id)
54+
def _send_scenario_for_program(scenario, program, scheduled_for, target_date):
55+
recipients = _get_recipients(scenario, program.id, target_date)
4556
if not recipients.exists():
4657
return 0
4758

@@ -88,10 +99,14 @@ def _send_scenario_for_program(scenario, program, scheduled_for):
8899
]
89100
MailingScenarioLog.objects.bulk_create(logs, ignore_conflicts=True)
90101

91-
deadline_date = _deadline_date(program)
102+
reference_date = (
103+
_deadline_date(program)
104+
if scenario.trigger == TriggerType.PROGRAM_SUBMISSION_DEADLINE
105+
else target_date
106+
)
92107

93108
def context_builder(user):
94-
return scenario.context_builder(program, user, deadline_date)
109+
return scenario.context_builder(program, user, reference_date)
95110

96111
sent_count = 0
97112
failed_count = 0
@@ -237,9 +252,14 @@ def run_program_mailings() -> int:
237252
today = timezone.localdate()
238253
total_sent = 0
239254
for scenario in SCENARIOS:
240-
target_date = today + timedelta(days=scenario.offset_days)
255+
if scenario.trigger == TriggerType.PROGRAM_SUBMISSION_DEADLINE:
256+
target_date = today + timedelta(days=scenario.offset_days)
257+
else:
258+
target_date = today - timedelta(days=scenario.offset_days)
241259
programs = _get_programs_for_scenario(scenario, target_date)
242260
for program in programs:
243-
total_sent += _send_scenario_for_program(scenario, program, today)
261+
total_sent += _send_scenario_for_program(
262+
scenario, program, today, target_date
263+
)
244264
logger.info("Program mailings sent: %s", total_sent)
245265
return total_sent

partner_programs/selectors.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ def programs_with_submission_deadline_on(target_date):
2121
)
2222

2323

24+
def programs_with_registrations_on(target_date):
25+
return PartnerProgram.objects.filter(
26+
partner_program_profiles__datetime_created__date=target_date
27+
).distinct()
28+
29+
2430
def _participant_profiles(program_id: int):
2531
return PartnerProgramUserProfile.objects.filter(
2632
partner_program_id=program_id, user__isnull=False
@@ -52,3 +58,43 @@ def program_participants_without_project(program_id: int):
5258
.values_list("user_id", flat=True)
5359
)
5460
return User.objects.filter(id__in=eligible_ids).distinct()
61+
62+
63+
def program_participants_without_project_registered_on(program_id: int, target_date):
64+
profiles = _participant_profiles(program_id).filter(
65+
datetime_created__date=target_date
66+
)
67+
leader_exists = Exists(
68+
PartnerProgramProject.objects.filter(
69+
partner_program_id=program_id,
70+
project__leader_id=OuterRef("user_id"),
71+
)
72+
)
73+
collab_exists = Exists(
74+
Collaborator.objects.filter(
75+
user_id=OuterRef("user_id"),
76+
project__program_links__partner_program_id=program_id,
77+
)
78+
)
79+
eligible_ids = (
80+
profiles.annotate(is_leader=leader_exists, is_collab=collab_exists)
81+
.filter(is_leader=False, is_collab=False)
82+
.values_list("user_id", flat=True)
83+
)
84+
return User.objects.filter(id__in=eligible_ids).distinct()
85+
86+
87+
def program_participants_with_unsubmitted_project(program_id: int):
88+
participant_ids = _participant_profiles(program_id).values_list(
89+
"user_id", flat=True
90+
)
91+
leader_ids = PartnerProgramProject.objects.filter(
92+
partner_program_id=program_id, submitted=False
93+
).values_list("project__leader_id", flat=True)
94+
collab_ids = Collaborator.objects.filter(
95+
project__program_links__partner_program_id=program_id,
96+
project__program_links__submitted=False,
97+
).values_list("user_id", flat=True)
98+
return User.objects.filter(id__in=participant_ids).filter(
99+
Q(id__in=leader_ids) | Q(id__in=collab_ids)
100+
).distinct()

procollab/celery.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
},
2121
"program_scenarios_mailings": {
2222
"task": "mailing.tasks.run_program_mailings",
23-
"schedule": crontab(minute=0, hour=12),
23+
"schedule": crontab(minute=0, hour=10),
2424
},
2525
"publish_finished_program_projects": {
2626
"task": "partner_programs.tasks.publish_finished_program_projects_task",

templates/email/generic-template-0.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@
261261

262262
<div
263263
style="font-family:sans-serif;font-size:20px;font-weight:400;line-height:150%;text-align:left;color:#8c888a;"
264-
>{{ text }}</div>
264+
>{{ text|linebreaksbr }}</div>
265265

266266
</td>
267267
</tr>
@@ -316,6 +316,7 @@
316316
>
317317
<tbody>
318318

319+
{% if button_text and button_link %}
319320
<tr>
320321
<td
321322
align="center" vertical-align="middle" style="font-size:0px;padding:0;word-break:break-word;"
@@ -341,6 +342,7 @@
341342

342343
</td>
343344
</tr>
345+
{% endif %}
344346

345347
</tbody>
346348
</table>
@@ -453,4 +455,4 @@
453455

454456
</body>
455457
</html>
456-
458+

0 commit comments

Comments
 (0)