Address bug with reset signoffs

This commit is contained in:
arkerekon 2023-01-11 14:57:07 -05:00
parent e3321000cb
commit 7ff75e5e58
1 changed files with 428 additions and 428 deletions

View File

@ -1,428 +1,428 @@
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
from typing import List, Match, Callable, TypeVar, Optional, Iterable, Any, Coroutine from typing import List, Match, Callable, TypeVar, Optional, Iterable, Any, Coroutine
from fuzzywuzzy import fuzz from fuzzywuzzy import fuzz
import hooks import hooks
from plugins import identifier, house_management, scroll_util from plugins import identifier, house_management, scroll_util
import client import client
import slack_util import slack_util
SHEET_ID = "1f9p4H7TWPm8rAM4v_qr2Vc6lBiFNEmR-quTY9UtxEBI" SHEET_ID = "1f9p4H7TWPm8rAM4v_qr2Vc6lBiFNEmR-quTY9UtxEBI"
MIN_RATIO = 80.0 MIN_RATIO = 80.0
async def alert_user(brother: scroll_util.Brother, saywhat: str) -> None: async def alert_user(brother: scroll_util.Brother, saywhat: str) -> None:
""" """
DM a brother saying something. Wrapper around several simpler methods DM a brother saying something. Wrapper around several simpler methods
""" """
# We do this as a for loop just in case multiple people reg. to same scroll for some reason (e.g. dup accounts) # We do this as a for loop just in case multiple people reg. to same scroll for some reason (e.g. dup accounts)
succ = False succ = False
for slack_id in await identifier.lookup_brother_userids(brother): for slack_id in await identifier.lookup_brother_userids(brother):
client.get_slack().send_message(saywhat, slack_id) client.get_slack().send_message(saywhat, slack_id)
succ = True succ = True
# Warn if we never find # Warn if we never find
if not succ: if not succ:
logging.warning("Unable to find dm conversation for brother {}".format(brother)) logging.warning("Unable to find dm conversation for brother {}".format(brother))
# Generic type # Generic type
T = TypeVar("T") T = TypeVar("T")
def tiemax(items: Iterable[T], key: Callable[[T], Optional[float]]) -> List[T]: def tiemax(items: Iterable[T], key: Callable[[T], Optional[float]]) -> List[T]:
best = [] best = []
best_score = None best_score = None
for elt in items: for elt in items:
# Compute the score # Compute the score
score = key(elt) score = key(elt)
# Ignore blank scores # Ignore blank scores
if score is None: if score is None:
continue continue
# Check if its the new best, wiping old if so # Check if its the new best, wiping old if so
elif best_score is None or score > best_score: elif best_score is None or score > best_score:
best_score = score best_score = score
best = [elt] best = [elt]
# Check if its same as last best # Check if its same as last best
elif score == best_score: elif score == best_score:
best.append(elt) best.append(elt)
return best return best
@dataclass @dataclass
class _ModJobContext: class _ModJobContext:
signer: scroll_util.Brother # The brother invoking the command signer: scroll_util.Brother # The brother invoking the command
assign: house_management.JobAssignment # The job assignment to modify assign: house_management.JobAssignment # The job assignment to modify
async def _mod_jobs(event: slack_util.Event, async def _mod_jobs(event: slack_util.Event,
relevance_scorer: Callable[[house_management.JobAssignment], Optional[float]], relevance_scorer: Callable[[house_management.JobAssignment], Optional[float]],
modifier: Callable[[_ModJobContext], Coroutine[Any, Any, None]], modifier: Callable[[_ModJobContext], Coroutine[Any, Any, None]],
no_job_msg: str = None no_job_msg: str = None
) -> None: ) -> None:
""" """
Stub function that handles various tasks relating to modifying jobs Stub function that handles various tasks relating to modifying jobs
:param relevance_scorer: Function scores job assignments on relevance. Determines which gets modified :param relevance_scorer: Function scores job assignments on relevance. Determines which gets modified
:param modifier: Callback function to modify a job. Only called on a successful operation, and only on one job :param modifier: Callback function to modify a job. Only called on a successful operation, and only on one job
""" """
# Make an error wrapper # Make an error wrapper
verb = slack_util.VerboseWrapper(event) verb = slack_util.VerboseWrapper(event)
# Who invoked this command? # Who invoked this command?
signer = await verb(event.user.as_user().get_brother()) signer = await verb(event.user.as_user().get_brother())
# Get all of the assignments # Get all of the assignments
assigns = await verb(house_management.import_assignments()) assigns = await verb(house_management.import_assignments())
# Find closest assignment to what we're after. This just wraps relevance_scorer to handle nones. # Find closest assignment to what we're after. This just wraps relevance_scorer to handle nones.
def none_scorer(a: Optional[house_management.JobAssignment]) -> Optional[float]: def none_scorer(a: Optional[house_management.JobAssignment]) -> Optional[float]:
if a is None: if a is None:
return None return None
else: else:
return relevance_scorer(a) return relevance_scorer(a)
closest_assigns = tiemax(assigns, key=none_scorer) closest_assigns = tiemax(assigns, key=none_scorer)
# This is what we do on success. It will or won't be called immediately based on what's in closest_assigns # This is what we do on success. It will or won't be called immediately based on what's in closest_assigns
async def success_callback(targ_assign: house_management.JobAssignment) -> None: async def success_callback(targ_assign: house_management.JobAssignment) -> None:
# First get the most up to date version of the jobs # First get the most up to date version of the jobs
fresh_assigns = await verb(house_management.import_assignments()) fresh_assigns = await verb(house_management.import_assignments())
# Find the one that matches what we had before # Find the one that matches what we had before
fresh_targ_assign = fresh_assigns[fresh_assigns.index(targ_assign)] fresh_targ_assign = fresh_assigns[fresh_assigns.index(targ_assign)]
# Create the context # Create the context
context = _ModJobContext(signer, fresh_targ_assign) context = _ModJobContext(signer, fresh_targ_assign)
# Modify it # Modify it
await modifier(context) await modifier(context)
# Re-upload # Re-upload
await house_management.export_assignments(fresh_assigns) await house_management.export_assignments(fresh_assigns)
# Also import and update points # Also import and update points
headers, points = await house_management.import_points() headers, points = await house_management.import_points()
house_management.apply_house_points(points, fresh_assigns) house_management.apply_house_points(points, fresh_assigns)
house_management.export_points(headers, points) house_management.export_points(headers, points)
# If there aren't any jobs, say so # If there aren't any jobs, say so
if len(closest_assigns) == 0: if len(closest_assigns) == 0:
if no_job_msg is None: if no_job_msg is None:
no_job_msg = "Unable to find any jobs to apply this command to. Try again with better spelling or whatever." no_job_msg = "Unable to find any jobs to apply this command to. Try again with better spelling or whatever."
client.get_slack().reply(event, no_job_msg) client.get_slack().reply(event, no_job_msg)
# If theres only one job, sign it off # If theres only one job, sign it off
elif len(closest_assigns) == 1: elif len(closest_assigns) == 1:
await success_callback(closest_assigns[0]) await success_callback(closest_assigns[0])
# If theres multiple jobs, we need to get a follow up! # If theres multiple jobs, we need to get a follow up!
else: else:
# Say we need more info # Say we need more info
job_list = "\n".join("{}: {}".format(i, a.job.pretty_fmt()) for i, a in enumerate(closest_assigns)) job_list = "\n".join("{}: {}".format(i, a.job.pretty_fmt()) for i, a in enumerate(closest_assigns))
client.get_slack().reply(event, "Multiple relevant job listings found.\n" client.get_slack().reply(event, "Multiple relevant job listings found.\n"
"Please enter the number corresponding to the job " "Please enter the number corresponding to the job "
"you wish to modify:\n{}".format(job_list)) "you wish to modify:\n{}".format(job_list))
# Establish a follow up command pattern # Establish a follow up command pattern
pattern = r"\d+" pattern = r"\d+"
# Make the follow up callback # Make the follow up callback
async def foc(_event: slack_util.Event, _match: Match) -> None: async def foc(_event: slack_util.Event, _match: Match) -> None:
# Get the number out # Get the number out
index = int(_match.group(0)) index = int(_match.group(0))
# Check that its valid # Check that its valid
if 0 <= index < len(closest_assigns): if 0 <= index < len(closest_assigns):
# We now know what we're trying to sign off! # We now know what we're trying to sign off!
await success_callback(closest_assigns[index]) await success_callback(closest_assigns[index])
else: else:
# They gave a bad index, or we were unable to find the assignment again. # They gave a bad index, or we were unable to find the assignment again.
client.get_slack().reply(_event, "Invalid job index / job unable to be found.") client.get_slack().reply(_event, "Invalid job index / job unable to be found.")
# Make a listener hook # Make a listener hook
new_hook = hooks.ReplyWaiter(foc, pattern, event.message.ts, 120) new_hook = hooks.ReplyWaiter(foc, pattern, event.message.ts, 120)
# Register it # Register it
client.get_slack().add_hook(new_hook) client.get_slack().add_hook(new_hook)
async def signoff_callback(event: slack_util.Event, match: Match) -> None: async def signoff_callback(event: slack_util.Event, match: Match) -> None:
verb = slack_util.VerboseWrapper(event) verb = slack_util.VerboseWrapper(event)
print("Receving a signoff request!!!") print("Receving a signoff request!!!")
# Find out who we are trying to sign off is # Find out who we are trying to sign off is
signee_name = match.group(1) signee_name = match.group(1)
signee = await verb(scroll_util.find_by_name(signee_name, MIN_RATIO)) signee = await verb(scroll_util.find_by_name(signee_name, MIN_RATIO))
# Score by name similarity, only accepting non-assigned jobs # Score by name similarity, only accepting non-assigned jobs
def scorer(assign: house_management.JobAssignment): def scorer(assign: house_management.JobAssignment):
if assign.assignee is not None: if assign.assignee is not None:
r = fuzz.ratio(signee.name, assign.assignee.name) r = fuzz.ratio(signee.name, assign.assignee.name)
if assign.signer is None and r > MIN_RATIO: if assign.signer is None and r > MIN_RATIO:
return r return r
# Set the assigner, and notify # Set the assigner, and notify
async def modifier(context: _ModJobContext): async def modifier(context: _ModJobContext):
context.assign.signer = context.signer context.assign.signer = context.signer
# Say we did it wooo! # Say we did it wooo!
client.get_slack().reply(event, "Signed off {} for {}".format(context.assign.assignee.name, client.get_slack().reply(event, "Signed off {} for {}".format(context.assign.assignee.name,
context.assign.job.name)) context.assign.job.name))
await alert_user(context.assign.assignee, "{} signed you off for {}.".format(context.assign.signer.name, await alert_user(context.assign.assignee, "{} signed you off for {}.".format(context.assign.signer.name,
context.assign.job.pretty_fmt())) context.assign.job.pretty_fmt()))
# Fire it off # Fire it off
await _mod_jobs(event, scorer, modifier) await _mod_jobs(event, scorer, modifier)
async def undo_callback(event: slack_util.Event, match: Match) -> None: async def undo_callback(event: slack_util.Event, match: Match) -> None:
verb = slack_util.VerboseWrapper(event) verb = slack_util.VerboseWrapper(event)
print("Receving an unsignoff request!!!") print("Receving an unsignoff request!!!")
# Find out who we are trying to sign off is # Find out who we are trying to sign off is
signee_name = match.group(1) signee_name = match.group(1)
signee = await verb(scroll_util.find_by_name(signee_name, MIN_RATIO)) signee = await verb(scroll_util.find_by_name(signee_name, MIN_RATIO))
# Score by name similarity, only accepting jobs that are signed off # Score by name similarity, only accepting jobs that are signed off
def scorer(assign: house_management.JobAssignment): def scorer(assign: house_management.JobAssignment):
if assign.assignee is not None: if assign.assignee is not None:
r = fuzz.ratio(signee.name, assign.assignee.name) r = fuzz.ratio(signee.name, assign.assignee.name)
if assign.signer is not None and r > MIN_RATIO: if assign.signer is not None and r > MIN_RATIO:
return r return r
# Set the assigner to be None, and notify # Set the assigner to be None, and notify
async def modifier(context: _ModJobContext): async def modifier(context: _ModJobContext):
context.assign.signer = None context.assign.signer = None
# Say we did it wooo! # Say we did it wooo!
client.get_slack().reply(event, "Undid signoff of {} for {}".format(context.assign.assignee.name, client.get_slack().reply(event, "Undid signoff of {} for {}".format(context.assign.assignee.name,
context.assign.job.name)) context.assign.job.name))
await alert_user(context.assign.assignee, "{} undid your signoff off for {}.\n") await alert_user(context.assign.assignee, "{} undid your signoff off for {}.\n")
# Fire it off # Fire it off
await _mod_jobs(event, scorer, modifier) await _mod_jobs(event, scorer, modifier)
async def late_callback(event: slack_util.Event, match: Match) -> None: async def late_callback(event: slack_util.Event, match: Match) -> None:
verb = slack_util.VerboseWrapper(event) verb = slack_util.VerboseWrapper(event)
# Find out who we are trying to sign off is # Find out who we are trying to sign off is
signee_name = match.group(1) signee_name = match.group(1)
signee = await verb(scroll_util.find_by_name(signee_name, MIN_RATIO)) signee = await verb(scroll_util.find_by_name(signee_name, MIN_RATIO))
# Score by name similarity. Don't care if signed off or not # Score by name similarity. Don't care if signed off or not
def scorer(assign: house_management.JobAssignment): def scorer(assign: house_management.JobAssignment):
if assign.assignee is not None: if assign.assignee is not None:
r = fuzz.ratio(signee.name, assign.assignee.name) r = fuzz.ratio(signee.name, assign.assignee.name)
if r > MIN_RATIO: if r > MIN_RATIO:
return r return r
# Just set the assigner # Just set the assigner
async def modifier(context: _ModJobContext): async def modifier(context: _ModJobContext):
context.assign.late = not context.assign.late context.assign.late = not context.assign.late
# Say we did it # Say we did it
client.get_slack().reply(event, "Toggled lateness of {}.\n" client.get_slack().reply(event, "Toggled lateness of {}.\n"
"Now marked as late: {}".format(context.assign.job.pretty_fmt(), "Now marked as late: {}".format(context.assign.job.pretty_fmt(),
context.assign.late)) context.assign.late))
# Fire it off # Fire it off
await _mod_jobs(event, scorer, modifier) await _mod_jobs(event, scorer, modifier)
async def reassign_callback(event: slack_util.Event, match: Match) -> None: async def reassign_callback(event: slack_util.Event, match: Match) -> None:
verb = slack_util.VerboseWrapper(event) verb = slack_util.VerboseWrapper(event)
# Find out our two targets # Find out our two targets
from_name = match.group(1).strip() from_name = match.group(1).strip()
to_name = match.group(2).strip() to_name = match.group(2).strip()
# Get them as brothers # Get them as brothers
from_bro = await verb(scroll_util.find_by_name(from_name, MIN_RATIO)) from_bro = await verb(scroll_util.find_by_name(from_name, MIN_RATIO))
to_bro = await verb(scroll_util.find_by_name(to_name, MIN_RATIO)) to_bro = await verb(scroll_util.find_by_name(to_name, MIN_RATIO))
# Score by name similarity to the first brother. Don't care if signed off or not, # Score by name similarity to the first brother. Don't care if signed off or not,
# as we want to be able to transfer even after signoffs (why not, amirite?) # as we want to be able to transfer even after signoffs (why not, amirite?)
def scorer(assign: house_management.JobAssignment): def scorer(assign: house_management.JobAssignment):
if assign.assignee is not None: if assign.assignee is not None:
r = fuzz.ratio(from_bro.name, assign.assignee.name) r = fuzz.ratio(from_bro.name, assign.assignee.name)
if r > MIN_RATIO: if r > MIN_RATIO:
return r return r
# Change the assignee # Change the assignee
async def modifier(context: _ModJobContext): async def modifier(context: _ModJobContext):
context.assign.assignee = to_bro context.assign.assignee = to_bro
# Say we did it # Say we did it
reassign_msg = "Job {} reassigned from {} to {}".format(context.assign.job.pretty_fmt(), reassign_msg = "Job {} reassigned from {} to {}".format(context.assign.job.pretty_fmt(),
from_bro, from_bro,
to_bro) to_bro)
client.get_slack().reply(event, reassign_msg) client.get_slack().reply(event, reassign_msg)
# Tell the people # Tell the people
reassign_msg = "Job {} reassigned from {} to {}".format(context.assign.job.pretty_fmt(), reassign_msg = "Job {} reassigned from {} to {}".format(context.assign.job.pretty_fmt(),
from_bro, from_bro,
to_bro) to_bro)
await alert_user(from_bro, reassign_msg) await alert_user(from_bro, reassign_msg)
await alert_user(to_bro, reassign_msg) await alert_user(to_bro, reassign_msg)
# Fire it off # Fire it off
await _mod_jobs(event, scorer, modifier) await _mod_jobs(event, scorer, modifier)
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
async def reset_callback(event: slack_util.Event, match: Match) -> None: async def reset_callback(event: slack_util.Event, match: Match) -> None:
""" """
Resets the scores. Resets the scores.
""" """
# Unassign everything # Unassign everything
assigns = await house_management.import_assignments() assigns = await house_management.import_assignments()
for a in assigns: for a in assigns:
if a is not None: if a is not None:
a.signer = None a.signer = None
await house_management.export_assignments(assigns) await house_management.export_assignments(assigns)
# Now wipe points # Now wipe points
headers, points = house_management.import_points() headers, points = await house_management.import_points()
# Set to 0/default # Set to 0/default
for i in range(len(points)): for i in range(len(points)):
new = house_management.PointStatus(brother=points[i].brother) new = house_management.PointStatus(brother=points[i].brother)
points[i] = new points[i] = new
house_management.apply_house_points(points, await house_management.import_assignments()) house_management.apply_house_points(points, await house_management.import_assignments())
house_management.export_points(headers, points) house_management.export_points(headers, points)
client.get_slack().reply(event, "Reset scores and signoffs") client.get_slack().reply(event, "Reset scores and signoffs")
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
async def refresh_callback(event: slack_util.Event, match: Match) -> None: async def refresh_callback(event: slack_util.Event, match: Match) -> None:
print("Received request to reset points") print("Received request to reset points")
headers, points = await house_management.import_points() headers, points = await house_management.import_points()
print("Received request to reset points 1") print("Received request to reset points 1")
house_management.apply_house_points(points, await house_management.import_assignments()) house_management.apply_house_points(points, await house_management.import_assignments())
print("Received request to reset points 2") print("Received request to reset points 2")
house_management.export_points(headers, points) house_management.export_points(headers, points)
print("Received request to reset points 3") print("Received request to reset points 3")
client.get_slack().reply(event, "Force updated point values") client.get_slack().reply(event, "Force updated point values")
async def nag_callback(event: slack_util.Event, match: Match) -> None: async def nag_callback(event: slack_util.Event, match: Match) -> None:
# Get the day # Get the day
day = match.group(1).lower().strip() day = match.group(1).lower().strip()
if not await nag_jobs(day): if not await nag_jobs(day):
client.get_slack().reply(event, client.get_slack().reply(event,
"No jobs found. Check that the day is spelled correctly, with no extra symbols.\n" "No jobs found. Check that the day is spelled correctly, with no extra symbols.\n"
"It is possible that all jobs have been signed off, as well.", "It is possible that all jobs have been signed off, as well.",
in_thread=True) in_thread=True)
# Wrapper so we can auto-call this as well # Wrapper so we can auto-call this as well
async def nag_jobs(day_of_week: str) -> bool: async def nag_jobs(day_of_week: str) -> bool:
# Get the assigns # Get the assigns
assigns = await house_management.import_assignments() assigns = await house_management.import_assignments()
# Filter to day # Filter to day
assigns = [assign for assign in assigns if assign is not None and assign.job.day_of_week.lower() == day_of_week] assigns = [assign for assign in assigns if assign is not None and assign.job.day_of_week.lower() == day_of_week]
# Filter signed off # Filter signed off
assigns = [assign for assign in assigns if assign.signer is None] assigns = [assign for assign in assigns if assign.signer is None]
# If no jobs found, somethings up. Probably mispelled day. Return failure # If no jobs found, somethings up. Probably mispelled day. Return failure
if not assigns: if not assigns:
return False return False
# Nag each # Nag each
response = "Do yer jerbs! They are as follows:\n" response = "Do yer jerbs! They are as follows:\n"
for assign in assigns: for assign in assigns:
# Make the row template # Make the row template
if assign.assignee is None: if assign.assignee is None:
continue continue
response += "({}) {} -- {} ".format(assign.job.house, assign.job.name, assign.assignee.name) response += "({}) {} -- {} ".format(assign.job.house, assign.job.name, assign.assignee.name)
# Find the people to @ # Find the people to @
brother_slack_ids = await identifier.lookup_brother_userids(assign.assignee) brother_slack_ids = await identifier.lookup_brother_userids(assign.assignee)
if brother_slack_ids: if brother_slack_ids:
for slack_id in brother_slack_ids: for slack_id in brother_slack_ids:
response += "<@{}> ".format(slack_id) response += "<@{}> ".format(slack_id)
else: else:
response += "(scroll missing. Please register for @ pings!)" response += "(scroll missing. Please register for @ pings!)"
response += "\n" response += "\n"
general_id = client.get_slack().get_conversation_by_name("#general").id general_id = client.get_slack().get_conversation_by_name("#general").id
client.get_slack().send_message(response, general_id) client.get_slack().send_message(response, general_id)
return True return True
signoff_hook = hooks.ChannelHook(signoff_callback, signoff_hook = hooks.ChannelHook(signoff_callback,
patterns=[ patterns=[
r"signoff\s+(.*)", r"signoff\s+(.*)",
r"sign off\s+(.*)", r"sign off\s+(.*)",
], ],
channel_whitelist=["#housejobs"]) channel_whitelist=["#housejobs"])
undo_hook = hooks.ChannelHook(undo_callback, undo_hook = hooks.ChannelHook(undo_callback,
patterns=[ patterns=[
r"unsignoff\s+(.*)", r"unsignoff\s+(.*)",
r"undosignoff\s+(.*)", r"undosignoff\s+(.*)",
r"undo signoff\s+(.*)", r"undo signoff\s+(.*)",
], ],
channel_whitelist=["#housejobs"]) channel_whitelist=["#housejobs"])
late_hook = hooks.ChannelHook(late_callback, late_hook = hooks.ChannelHook(late_callback,
patterns=[ patterns=[
r"marklate\s+(.*)", r"marklate\s+(.*)",
r"mark late\s+(.*)", r"mark late\s+(.*)",
], ],
channel_whitelist=["#housejobs"]) channel_whitelist=["#housejobs"])
reset_hook = hooks.ChannelHook(reset_callback, reset_hook = hooks.ChannelHook(reset_callback,
patterns=[ patterns=[
r"reset signoffs", r"reset signoffs",
r"reset sign offs", r"reset sign offs",
], ],
channel_whitelist=["#command-center"]) channel_whitelist=["#command-center"])
nag_hook = hooks.ChannelHook(nag_callback, nag_hook = hooks.ChannelHook(nag_callback,
patterns=[ patterns=[
r"nagjobs\s+(.*)", r"nagjobs\s+(.*)",
r"nag jobs\s+(.*)" r"nag jobs\s+(.*)"
], ],
channel_whitelist=["#command-center"]) channel_whitelist=["#command-center"])
reassign_hook = hooks.ChannelHook(reassign_callback, reassign_hook = hooks.ChannelHook(reassign_callback,
patterns=r"reassign\s+(.*?)-&gt;\s+(.+)", patterns=r"reassign\s+(.*?)-&gt;\s+(.+)",
channel_whitelist=["#housejobs"]) channel_whitelist=["#housejobs"])
refresh_hook = hooks.ChannelHook(refresh_callback, refresh_hook = hooks.ChannelHook(refresh_callback,
patterns=[ patterns=[
"refresh points", "refresh points",
"update points" "update points"
], ],
channel_whitelist=["#command-center"]) channel_whitelist=["#command-center"])
block_action = """ block_action = """
[ [
{ {
"type": "actions", "type": "actions",
"block_id": "test_block_id", "block_id": "test_block_id",
"elements": [ "elements": [
{ {
"type": "button", "type": "button",
"action_id": "test_action_id", "action_id": "test_action_id",
"text": { "text": {
"type": "plain_text", "type": "plain_text",
"text": "Send payload", "text": "Send payload",
"emoji": false "emoji": false
} }
} }
] ]
} }
] ]
""" """