Skip to content

Commit f530429

Browse files
committed
Ask members if they don't want more task suggestions, if they persistently ignore them
1 parent 8aec97b commit f530429

3 files changed

Lines changed: 139 additions & 4 deletions

File tree

api/src/messages/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class MessageTemplate(enum.Enum):
4040
MEMBERBOOTH_BOX_EXPIRED = "memberbooth_box_expired"
4141
MEMBERBOOTH_BOX_EXPIRING_SOON = "memberbooth_box_expiring_soon"
4242
QUIZ_COMPLETION = "quiz_completion"
43+
TASK_DELEGATION_PREFERENCE = "task_delegation_preference"
4344

4445

4546
class Message(Base):

api/src/tasks/delegate.py

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import math
23
import re
34
import time
@@ -11,6 +12,8 @@
1112

1213
import requests
1314
from membership.models import Group, Member, member_group
15+
from messages.message import send_message
16+
from messages.models import Message, MessageTemplate
1417
from multiaccessy.models import PhysicalAccessEntry
1518
from PIL import Image
1619
from quiz.models import QuizAnswer, QuizQuestion
@@ -1164,6 +1167,19 @@ def is_task_delegation_enabled(member_id: int) -> bool:
11641167
return pref.selected_options == "true"
11651168

11661169

1170+
def consecutive_ignored_tasks(last_assigned_tasks: list[TaskDelegationLogSimple]) -> list[TaskDelegationLogSimple]:
1171+
"""Return the leading run of ignored tasks (newest first), skipping still-pending and re-rolled tasks."""
1172+
result = []
1173+
for task in last_assigned_tasks:
1174+
if task.status in ("assigned", "not_done_rerolled"):
1175+
continue
1176+
if task.status == "ignored":
1177+
result.append(task)
1178+
else:
1179+
break
1180+
return result
1181+
1182+
11671183
def member_recently_received_task(ctx: TaskContext) -> bool:
11681184
# If a member has completed a task recently, don't assign them another one too soon.
11691185
# The wait depends on the size of the task
@@ -1183,10 +1199,11 @@ def member_recently_received_task(ctx: TaskContext) -> bool:
11831199
# It's then no longer assigned or completed, but we still don't want to assign them another task too soon.
11841200
#
11851201
# If the member explicitly wanted a new task, we allow assigning a new task sooner.
1186-
if (
1187-
ctx.time - last_assigned_task.created_at < timedelta(hours=14)
1188-
and last_assigned_task.status != "not_done_rerolled"
1189-
):
1202+
# If the member has been ignoring tasks, increase the delay by 3x.
1203+
base_delay = timedelta(hours=14)
1204+
if len(consecutive_ignored_tasks(ctx.member.last_assigned_tasks)) >= 3:
1205+
base_delay *= 3
1206+
if ctx.time - last_assigned_task.created_at < base_delay and last_assigned_task.status != "not_done_rerolled":
11901207
return True
11911208

11921209
return False
@@ -1800,6 +1817,16 @@ def delegate_task_for_member(
18001817
logger.info(f"Member {member_id} received a task recently; skipping delegation")
18011818
return None
18021819

1820+
# If 3 tasks have been ignored in a row since the last preference question, ask again.
1821+
if not force and slack_interaction is None:
1822+
if should_ask_delegation_preference(member_id, ctx.member.last_assigned_tasks):
1823+
member = db_session.get(Member, member_id)
1824+
if member:
1825+
ask_delegation_preference_question(member)
1826+
db_session.commit()
1827+
logger.info(f"Queued delegation preference question for member {member_id}")
1828+
return None
1829+
18031830
if picked_card is None:
18041831
picked_card = select_card_for_member(ctx, ignore_reasons)
18051832

@@ -2010,6 +2037,24 @@ def clear_pending_question(member_id: int) -> None:
20102037
redis_connection.delete(cache_key)
20112038

20122039

2040+
def should_ask_delegation_preference(member_id: int, last_assigned_tasks: list[TaskDelegationLogSimple]) -> bool:
2041+
"""Return True if 3 tasks have been ignored in a row since the last delegation preference message was sent."""
2042+
streak = consecutive_ignored_tasks(last_assigned_tasks)
2043+
if len(streak) < 3:
2044+
return False
2045+
last_message_date = db_session.execute(
2046+
select(Message.created_at)
2047+
.where(
2048+
Message.member_id == member_id,
2049+
Message.template == MessageTemplate.TASK_DELEGATION_PREFERENCE.value,
2050+
Message.status != Message.FAILED,
2051+
)
2052+
.order_by(Message.created_at.desc())
2053+
.limit(1)
2054+
).scalar_one_or_none()
2055+
return last_message_date is None or streak[2].created_at > last_message_date
2056+
2057+
20132058
def ask_room_preference_question(
20142059
member: Member,
20152060
slack_client: WebClient,
@@ -2179,3 +2224,44 @@ def ask_skill_level_question(
21792224
channel = slack_response.get("channel", "")
21802225
ts = slack_response.get("ts", "")
21812226
return (channel, ts)
2227+
2228+
2229+
def ask_delegation_preference_question(member: Member) -> None:
2230+
"""Queue a Slack message asking the member whether they want to continue receiving task suggestions."""
2231+
text = (
2232+
f"Hi {member.firstname}! We've noticed you haven't responded to the last few task suggestions. "
2233+
f"Would you like to continue receiving task suggestions?"
2234+
)
2235+
2236+
blocks: list[Block] = [
2237+
DividerBlock(),
2238+
SectionBlock(text=MarkdownTextObject(text=text)),
2239+
ActionsBlock(
2240+
block_id="task_delegation_preference",
2241+
elements=[
2242+
ButtonElement(
2243+
text=PlainTextObject(text="Yes, keep sending tasks", emoji=True),
2244+
action_id="task_delegation_preference_yes",
2245+
value="yes",
2246+
style="primary",
2247+
),
2248+
ButtonElement(
2249+
text=PlainTextObject(text="No, stop sending tasks", emoji=True),
2250+
action_id="task_delegation_preference_no",
2251+
value="no",
2252+
style="danger",
2253+
),
2254+
],
2255+
),
2256+
]
2257+
2258+
send_message(
2259+
template=MessageTemplate.TASK_DELEGATION_PREFERENCE,
2260+
member=member,
2261+
db_session=db_session,
2262+
recipient=member.email,
2263+
recipient_type="slack",
2264+
subject="Continue receiving task suggestions?",
2265+
body=json.dumps([block.to_dict() for block in blocks]),
2266+
)
2267+
logger.info(f"Queued delegation preference question for member #{member.member_number}")

api/src/tasks/views.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,8 @@ def slack_interaction() -> dict:
689689
return slack_handle_new_task_request(payload)
690690
elif action.action_id in ["slack_email_confirm", "slack_email_cancel"]:
691691
return slack_handle_email_verification(payload)
692+
elif action.action_id.startswith("task_delegation_preference_"):
693+
return slack_handle_delegation_preference(payload)
692694
else:
693695
return slack_handle_task_feedback(payload)
694696

@@ -706,6 +708,52 @@ def slack_handle_new_task_request(payload: SlackInteraction) -> dict:
706708
return {"message": "New task assigned"}
707709

708710

711+
def slack_handle_delegation_preference(payload: SlackInteraction) -> dict:
712+
"""Handle when a member responds to the 'continue receiving tasks?' question."""
713+
action = payload.actions[0]
714+
slack_client = get_slack_client()
715+
member = member_from_slack_user_id(slack_client, payload.user.id)
716+
717+
if not member:
718+
raise UnprocessableEntity("Member not found")
719+
720+
if action.action_id == "task_delegation_preference_no":
721+
pref = MemberPreference(
722+
member_id=member.member_id,
723+
question_type=MemberPreferenceQuestionType.TASK_DELEGATION_ENABLED,
724+
available_options="true,false",
725+
selected_options="false",
726+
)
727+
db_session.add(pref)
728+
db_session.commit()
729+
730+
slack_client.chat_update(
731+
channel=payload.channel.id,
732+
ts=payload.message.ts,
733+
text="Task suggestions disabled. You can re-enable them at any time in the member portal.",
734+
blocks=[
735+
SectionBlock(
736+
text=MarkdownTextObject(
737+
text=":no_bell: Got it! We'll stop sending you task suggestions. You can re-enable them at any time in the member portal."
738+
)
739+
)
740+
],
741+
)
742+
else:
743+
# "yes" — pref_question_ignore_count is already set to the current count when we asked,
744+
# so we just need to confirm to the member.
745+
slack_client.chat_update(
746+
channel=payload.channel.id,
747+
ts=payload.message.ts,
748+
text="Great! We'll keep sending you task suggestions.",
749+
blocks=[
750+
SectionBlock(text=MarkdownTextObject(text=":bell: Great! We'll keep sending you task suggestions."))
751+
],
752+
)
753+
754+
return {"message": "Preference saved"}
755+
756+
709757
def slack_handle_email_verification(payload: SlackInteraction) -> dict:
710758
"""Handle Slack email verification confirmation or cancellation."""
711759
from membership.models import SlackEmailOverride

0 commit comments

Comments
 (0)